##// END OF EJS Templates
python3: fix urllib usage
super-admin -
r4914:49ad81de default
parent child Browse files
Show More

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

@@ -1,171 +1,171 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import os
22 22 import csv
23 23 import datetime
24 24
25 25 import pytest
26 26
27 27 from rhodecode.tests import *
28 28 from rhodecode.tests.fixture import FIXTURES
29 29 from rhodecode.model.db import UserLog
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.lib.utils2 import safe_unicode
32 32
33 33
34 34 def route_path(name, params=None, **kwargs):
35 import urllib
35 import urllib.request, urllib.parse, urllib.error
36 36 from rhodecode.apps._base import ADMIN_PREFIX
37 37
38 38 base_url = {
39 39 'admin_home': ADMIN_PREFIX,
40 40 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
41 41
42 42 }[name].format(**kwargs)
43 43
44 44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 46 return base_url
47 47
48 48
49 49 @pytest.mark.usefixtures('app')
50 50 class TestAdminController(object):
51 51
52 52 @pytest.fixture(scope='class', autouse=True)
53 53 def prepare(self, request, baseapp):
54 54 UserLog.query().delete()
55 55 Session().commit()
56 56
57 57 def strptime(val):
58 58 fmt = '%Y-%m-%d %H:%M:%S'
59 59 if '.' not in val:
60 60 return datetime.datetime.strptime(val, fmt)
61 61
62 62 nofrag, frag = val.split(".")
63 63 date = datetime.datetime.strptime(nofrag, fmt)
64 64
65 65 frag = frag[:6] # truncate to microseconds
66 66 frag += (6 - len(frag)) * '0' # add 0s
67 67 return date.replace(microsecond=int(frag))
68 68
69 69 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
70 70 for row in csv.DictReader(f):
71 71 ul = UserLog()
72 72 for k, v in row.iteritems():
73 73 v = safe_unicode(v)
74 74 if k == 'action_date':
75 75 v = strptime(v)
76 76 if k in ['user_id', 'repository_id']:
77 77 # nullable due to FK problems
78 78 v = None
79 79 setattr(ul, k, v)
80 80 Session().add(ul)
81 81 Session().commit()
82 82
83 83 @request.addfinalizer
84 84 def cleanup():
85 85 UserLog.query().delete()
86 86 Session().commit()
87 87
88 88 def test_index(self, autologin_user):
89 89 response = self.app.get(route_path('admin_audit_logs'))
90 90 response.mustcontain('Admin audit logs')
91 91
92 92 def test_filter_all_entries(self, autologin_user):
93 93 response = self.app.get(route_path('admin_audit_logs'))
94 94 all_count = UserLog.query().count()
95 95 response.mustcontain('%s entries' % all_count)
96 96
97 97 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
98 98 response = self.app.get(route_path('admin_audit_logs',
99 99 params=dict(filter='repository:rhodecode')))
100 100 response.mustcontain('3 entries')
101 101
102 102 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
103 103 response = self.app.get(route_path('admin_audit_logs',
104 104 params=dict(filter='repository:RhodeCode')))
105 105 response.mustcontain('3 entries')
106 106
107 107 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
108 108 response = self.app.get(route_path('admin_audit_logs',
109 109 params=dict(filter='repository:*test*')))
110 110 response.mustcontain('862 entries')
111 111
112 112 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
113 113 response = self.app.get(route_path('admin_audit_logs',
114 114 params=dict(filter='repository:test*')))
115 115 response.mustcontain('257 entries')
116 116
117 117 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
118 118 response = self.app.get(route_path('admin_audit_logs',
119 119 params=dict(filter='repository:Test*')))
120 120 response.mustcontain('257 entries')
121 121
122 122 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
123 123 response = self.app.get(route_path('admin_audit_logs',
124 124 params=dict(filter='repository:test* AND username:demo')))
125 125 response.mustcontain('130 entries')
126 126
127 127 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
128 128 response = self.app.get(route_path('admin_audit_logs',
129 129 params=dict(filter='repository:test* OR repository:rhodecode')))
130 130 response.mustcontain('260 entries') # 257 + 3
131 131
132 132 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
133 133 response = self.app.get(route_path('admin_audit_logs',
134 134 params=dict(filter='username:demo')))
135 135 response.mustcontain('1087 entries')
136 136
137 137 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
138 138 response = self.app.get(route_path('admin_audit_logs',
139 139 params=dict(filter='username:DemO')))
140 140 response.mustcontain('1087 entries')
141 141
142 142 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
143 143 response = self.app.get(route_path('admin_audit_logs',
144 144 params=dict(filter='username:*test*')))
145 145 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
146 146 response.mustcontain('{} entries'.format(entries_count))
147 147
148 148 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
149 149 response = self.app.get(route_path('admin_audit_logs',
150 150 params=dict(filter='username:demo*')))
151 151 response.mustcontain('1101 entries')
152 152
153 153 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
154 154 response = self.app.get(route_path('admin_audit_logs',
155 155 params=dict(filter='username:demo OR username:volcan')))
156 156 response.mustcontain('1095 entries') # 1087 + 8
157 157
158 158 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
159 159 response = self.app.get(route_path('admin_audit_logs',
160 160 params=dict(filter='action:*pull_request*')))
161 161 response.mustcontain('187 entries')
162 162
163 163 def test_filter_journal_filter_on_date(self, autologin_user):
164 164 response = self.app.get(route_path('admin_audit_logs',
165 165 params=dict(filter='date:20121010')))
166 166 response.mustcontain('47 entries')
167 167
168 168 def test_filter_journal_filter_on_date_2(self, autologin_user):
169 169 response = self.app.get(route_path('admin_audit_logs',
170 170 params=dict(filter='date:20121020')))
171 171 response.mustcontain('17 entries')
@@ -1,85 +1,85 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests import assert_session_flash
24 24 from rhodecode.model.settings import SettingsModel
25 25
26 26
27 27 def route_path(name, params=None, **kwargs):
28 import urllib
28 import urllib.request, urllib.parse, urllib.error
29 29 from rhodecode.apps._base import ADMIN_PREFIX
30 30
31 31 base_url = {
32 32 'admin_defaults_repositories':
33 33 ADMIN_PREFIX + '/defaults/repositories',
34 34 'admin_defaults_repositories_update':
35 35 ADMIN_PREFIX + '/defaults/repositories/update',
36 36 }[name].format(**kwargs)
37 37
38 38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 40 return base_url
41 41
42 42
43 43 @pytest.mark.usefixtures("app")
44 44 class TestDefaultsView(object):
45 45
46 46 def test_index(self, autologin_user):
47 47 response = self.app.get(route_path('admin_defaults_repositories'))
48 48 response.mustcontain('default_repo_private')
49 49 response.mustcontain('default_repo_enable_statistics')
50 50 response.mustcontain('default_repo_enable_downloads')
51 51 response.mustcontain('default_repo_enable_locking')
52 52
53 53 def test_update_params_true_hg(self, autologin_user, csrf_token):
54 54 params = {
55 55 'default_repo_enable_locking': True,
56 56 'default_repo_enable_downloads': True,
57 57 'default_repo_enable_statistics': True,
58 58 'default_repo_private': True,
59 59 'default_repo_type': 'hg',
60 60 'csrf_token': csrf_token,
61 61 }
62 62 response = self.app.post(
63 63 route_path('admin_defaults_repositories_update'), params=params)
64 64 assert_session_flash(response, 'Default settings updated successfully')
65 65
66 66 defs = SettingsModel().get_default_repo_settings()
67 67 del params['csrf_token']
68 68 assert params == defs
69 69
70 70 def test_update_params_false_git(self, autologin_user, csrf_token):
71 71 params = {
72 72 'default_repo_enable_locking': False,
73 73 'default_repo_enable_downloads': False,
74 74 'default_repo_enable_statistics': False,
75 75 'default_repo_private': False,
76 76 'default_repo_type': 'git',
77 77 'csrf_token': csrf_token,
78 78 }
79 79 response = self.app.post(
80 80 route_path('admin_defaults_repositories_update'), params=params)
81 81 assert_session_flash(response, 'Default settings updated successfully')
82 82
83 83 defs = SettingsModel().get_default_repo_settings()
84 84 del params['csrf_token']
85 85 assert params == defs
@@ -1,82 +1,82 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests import TestController
24 24 from rhodecode.tests.fixture import Fixture
25 25
26 26 fixture = Fixture()
27 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 import urllib
30 import urllib.request, urllib.parse, urllib.error
31 31 from rhodecode.apps._base import ADMIN_PREFIX
32 32
33 33 base_url = {
34 34 'admin_home': ADMIN_PREFIX,
35 35 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
36 36 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
37 37 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
38 38 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
39 39
40 40 }[name].format(**kwargs)
41 41
42 42 if params:
43 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 44 return base_url
45 45
46 46
47 47 class TestAdminMainView(TestController):
48 48
49 49 def test_access_admin_home(self):
50 50 self.log_user()
51 51 response = self.app.get(route_path('admin_home'), status=200)
52 52 response.mustcontain("Administration area")
53 53
54 54 def test_redirect_pull_request_view(self, view):
55 55 self.log_user()
56 56 self.app.get(
57 57 route_path(view, pull_request_id='xxxx'),
58 58 status=404)
59 59
60 60 @pytest.mark.backends("git", "hg")
61 61 @pytest.mark.parametrize('view', [
62 62 'pull_requests_global',
63 63 'pull_requests_global_0',
64 64 'pull_requests_global_1',
65 65 ])
66 66 def test_redirect_pull_request_view(self, view, pr_util):
67 67 self.log_user()
68 68 pull_request = pr_util.create_pull_request()
69 69 pull_request_id = pull_request.pull_request_id
70 70 repo_name = pull_request.target_repo.repo_name
71 71
72 72 response = self.app.get(
73 73 route_path(view, pull_request_id=pull_request_id),
74 74 status=302)
75 75 assert response.location.endswith(
76 76 'pull-request/{}'.format(pull_request_id))
77 77
78 78 redirect_url = route_path(
79 79 'pullrequest_show', repo_name=repo_name,
80 80 pull_request_id=pull_request_id)
81 81
82 82 assert redirect_url in response.location
@@ -1,299 +1,299 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import mock
22 22 import pytest
23 23 from rhodecode.model.db import User, UserIpMap
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.model.permission import PermissionModel
26 26 from rhodecode.model.ssh_key import SshKeyModel
27 27 from rhodecode.tests import (
28 28 TestController, clear_cache_regions, assert_session_flash)
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 33 from rhodecode.apps._base import ADMIN_PREFIX
34 34
35 35 base_url = {
36 36 'edit_user_ips':
37 37 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
38 38 'edit_user_ips_add':
39 39 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
40 40 'edit_user_ips_delete':
41 41 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
42 42
43 43 'admin_permissions_application':
44 44 ADMIN_PREFIX + '/permissions/application',
45 45 'admin_permissions_application_update':
46 46 ADMIN_PREFIX + '/permissions/application/update',
47 47
48 48 'admin_permissions_global':
49 49 ADMIN_PREFIX + '/permissions/global',
50 50 'admin_permissions_global_update':
51 51 ADMIN_PREFIX + '/permissions/global/update',
52 52
53 53 'admin_permissions_object':
54 54 ADMIN_PREFIX + '/permissions/object',
55 55 'admin_permissions_object_update':
56 56 ADMIN_PREFIX + '/permissions/object/update',
57 57
58 58 'admin_permissions_ips':
59 59 ADMIN_PREFIX + '/permissions/ips',
60 60 'admin_permissions_overview':
61 61 ADMIN_PREFIX + '/permissions/overview',
62 62
63 63 'admin_permissions_ssh_keys':
64 64 ADMIN_PREFIX + '/permissions/ssh_keys',
65 65 'admin_permissions_ssh_keys_data':
66 66 ADMIN_PREFIX + '/permissions/ssh_keys/data',
67 67 'admin_permissions_ssh_keys_update':
68 68 ADMIN_PREFIX + '/permissions/ssh_keys/update'
69 69
70 70 }[name].format(**kwargs)
71 71
72 72 if params:
73 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
73 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
74 74 return base_url
75 75
76 76
77 77 class TestAdminPermissionsController(TestController):
78 78
79 79 @pytest.fixture(scope='class', autouse=True)
80 80 def prepare(self, request):
81 81 # cleanup and reset to default permissions after
82 82 @request.addfinalizer
83 83 def cleanup():
84 84 PermissionModel().create_default_user_permissions(
85 85 User.get_default_user(), force=True)
86 86
87 87 def test_index_application(self):
88 88 self.log_user()
89 89 self.app.get(route_path('admin_permissions_application'))
90 90
91 91 @pytest.mark.parametrize(
92 92 'anonymous, default_register, default_register_message, default_password_reset,'
93 93 'default_extern_activate, expect_error, expect_form_error', [
94 94 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
95 95 False, False),
96 96 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
97 97 False, False),
98 98 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
99 99 False, False),
100 100 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
101 101 False, False),
102 102 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
103 103 False, True),
104 104 (True, '', '', 'hg.password_reset.enabled', '', True, False),
105 105 ])
106 106 def test_update_application_permissions(
107 107 self, anonymous, default_register, default_register_message, default_password_reset,
108 108 default_extern_activate, expect_error, expect_form_error):
109 109
110 110 self.log_user()
111 111
112 112 # TODO: anonymous access set here to False, breaks some other tests
113 113 params = {
114 114 'csrf_token': self.csrf_token,
115 115 'anonymous': anonymous,
116 116 'default_register': default_register,
117 117 'default_register_message': default_register_message,
118 118 'default_password_reset': default_password_reset,
119 119 'default_extern_activate': default_extern_activate,
120 120 }
121 121 response = self.app.post(route_path('admin_permissions_application_update'),
122 122 params=params)
123 123 if expect_form_error:
124 124 assert response.status_int == 200
125 125 response.mustcontain('Value must be one of')
126 126 else:
127 127 if expect_error:
128 128 msg = 'Error occurred during update of permissions'
129 129 else:
130 130 msg = 'Application permissions updated successfully'
131 131 assert_session_flash(response, msg)
132 132
133 133 def test_index_object(self):
134 134 self.log_user()
135 135 self.app.get(route_path('admin_permissions_object'))
136 136
137 137 @pytest.mark.parametrize(
138 138 'repo, repo_group, user_group, expect_error, expect_form_error', [
139 139 ('repository.none', 'group.none', 'usergroup.none', False, False),
140 140 ('repository.read', 'group.read', 'usergroup.read', False, False),
141 141 ('repository.write', 'group.write', 'usergroup.write',
142 142 False, False),
143 143 ('repository.admin', 'group.admin', 'usergroup.admin',
144 144 False, False),
145 145 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
146 146 ('', '', '', True, False),
147 147 ])
148 148 def test_update_object_permissions(self, repo, repo_group, user_group,
149 149 expect_error, expect_form_error):
150 150 self.log_user()
151 151
152 152 params = {
153 153 'csrf_token': self.csrf_token,
154 154 'default_repo_perm': repo,
155 155 'overwrite_default_repo': False,
156 156 'default_group_perm': repo_group,
157 157 'overwrite_default_group': False,
158 158 'default_user_group_perm': user_group,
159 159 'overwrite_default_user_group': False,
160 160 }
161 161 response = self.app.post(route_path('admin_permissions_object_update'),
162 162 params=params)
163 163 if expect_form_error:
164 164 assert response.status_int == 200
165 165 response.mustcontain('Value must be one of')
166 166 else:
167 167 if expect_error:
168 168 msg = 'Error occurred during update of permissions'
169 169 else:
170 170 msg = 'Object permissions updated successfully'
171 171 assert_session_flash(response, msg)
172 172
173 173 def test_index_global(self):
174 174 self.log_user()
175 175 self.app.get(route_path('admin_permissions_global'))
176 176
177 177 @pytest.mark.parametrize(
178 178 'repo_create, repo_create_write, user_group_create, repo_group_create,'
179 179 'fork_create, inherit_default_permissions, expect_error,'
180 180 'expect_form_error', [
181 181 ('hg.create.none', 'hg.create.write_on_repogroup.false',
182 182 'hg.usergroup.create.false', 'hg.repogroup.create.false',
183 183 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
184 184 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
185 185 'hg.usergroup.create.true', 'hg.repogroup.create.true',
186 186 'hg.fork.repository', 'hg.inherit_default_perms.false',
187 187 False, False),
188 188 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
189 189 'hg.usergroup.create.true', 'hg.repogroup.create.true',
190 190 'hg.fork.repository', 'hg.inherit_default_perms.false',
191 191 False, True),
192 192 ('', '', '', '', '', '', True, False),
193 193 ])
194 194 def test_update_global_permissions(
195 195 self, repo_create, repo_create_write, user_group_create,
196 196 repo_group_create, fork_create, inherit_default_permissions,
197 197 expect_error, expect_form_error):
198 198 self.log_user()
199 199
200 200 params = {
201 201 'csrf_token': self.csrf_token,
202 202 'default_repo_create': repo_create,
203 203 'default_repo_create_on_write': repo_create_write,
204 204 'default_user_group_create': user_group_create,
205 205 'default_repo_group_create': repo_group_create,
206 206 'default_fork_create': fork_create,
207 207 'default_inherit_default_permissions': inherit_default_permissions
208 208 }
209 209 response = self.app.post(route_path('admin_permissions_global_update'),
210 210 params=params)
211 211 if expect_form_error:
212 212 assert response.status_int == 200
213 213 response.mustcontain('Value must be one of')
214 214 else:
215 215 if expect_error:
216 216 msg = 'Error occurred during update of permissions'
217 217 else:
218 218 msg = 'Global permissions updated successfully'
219 219 assert_session_flash(response, msg)
220 220
221 221 def test_index_ips(self):
222 222 self.log_user()
223 223 response = self.app.get(route_path('admin_permissions_ips'))
224 224 response.mustcontain('All IP addresses are allowed')
225 225
226 226 def test_add_delete_ips(self):
227 227 clear_cache_regions(['sql_cache_short'])
228 228 self.log_user()
229 229
230 230 # ADD
231 231 default_user_id = User.get_default_user_id()
232 232 self.app.post(
233 233 route_path('edit_user_ips_add', user_id=default_user_id),
234 234 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
235 235
236 236 response = self.app.get(route_path('admin_permissions_ips'))
237 237 response.mustcontain('0.0.0.0/24')
238 238 response.mustcontain('0.0.0.0 - 0.0.0.255')
239 239
240 240 # DELETE
241 241 default_user_id = User.get_default_user_id()
242 242 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
243 243 default_user_id).first().ip_id
244 244
245 245 response = self.app.post(
246 246 route_path('edit_user_ips_delete', user_id=default_user_id),
247 247 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
248 248
249 249 assert_session_flash(response, 'Removed ip address from user whitelist')
250 250
251 251 clear_cache_regions(['sql_cache_short'])
252 252 response = self.app.get(route_path('admin_permissions_ips'))
253 253 response.mustcontain('All IP addresses are allowed')
254 254 response.mustcontain(no=['0.0.0.0/24'])
255 255 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
256 256
257 257 def test_index_overview(self):
258 258 self.log_user()
259 259 self.app.get(route_path('admin_permissions_overview'))
260 260
261 261 def test_ssh_keys(self):
262 262 self.log_user()
263 263 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
264 264
265 265 def test_ssh_keys_data(self, user_util, xhr_header):
266 266 self.log_user()
267 267 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
268 268 extra_environ=xhr_header)
269 269 assert response.json == {u'data': [], u'draw': None,
270 270 u'recordsFiltered': 0, u'recordsTotal': 0}
271 271
272 272 dummy_user = user_util.create_user()
273 273 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
274 274 Session().commit()
275 275 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
276 276 extra_environ=xhr_header)
277 277 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
278 278
279 279 def test_ssh_keys_update(self):
280 280 self.log_user()
281 281 response = self.app.post(
282 282 route_path('admin_permissions_ssh_keys_update'),
283 283 dict(csrf_token=self.csrf_token), status=302)
284 284
285 285 assert_session_flash(
286 286 response, 'Updated SSH keys file')
287 287
288 288 def test_ssh_keys_update_disabled(self):
289 289 self.log_user()
290 290
291 291 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
292 292 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
293 293 return_value=False):
294 294 response = self.app.post(
295 295 route_path('admin_permissions_ssh_keys_update'),
296 296 dict(csrf_token=self.csrf_token), status=302)
297 297
298 298 assert_session_flash(
299 299 response, 'SSH key support is disabled in .ini file') No newline at end of file
@@ -1,512 +1,512 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 urllib
21 import urllib.request, urllib.parse, urllib.error
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps._base import ADMIN_PREFIX
27 27 from rhodecode.lib import auth
28 28 from rhodecode.lib.utils2 import safe_str
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.model.db import (
31 31 Repository, RepoGroup, UserRepoToPerm, User, Permission)
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.repo import RepoModel
34 34 from rhodecode.model.repo_group import RepoGroupModel
35 35 from rhodecode.model.user import UserModel
36 36 from rhodecode.tests import (
37 37 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
38 38 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
39 39 from rhodecode.tests.fixture import Fixture, error_function
40 40 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
41 41
42 42 fixture = Fixture()
43 43
44 44
45 45 def route_path(name, params=None, **kwargs):
46 import urllib
46 import urllib.request, urllib.parse, urllib.error
47 47
48 48 base_url = {
49 49 'repos': ADMIN_PREFIX + '/repos',
50 50 'repos_data': ADMIN_PREFIX + '/repos_data',
51 51 'repo_new': ADMIN_PREFIX + '/repos/new',
52 52 'repo_create': ADMIN_PREFIX + '/repos/create',
53 53
54 54 'repo_creating_check': '/{repo_name}/repo_creating_check',
55 55 }[name].format(**kwargs)
56 56
57 57 if params:
58 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
58 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
59 59 return base_url
60 60
61 61
62 62 def _get_permission_for_user(user, repo):
63 63 perm = UserRepoToPerm.query()\
64 64 .filter(UserRepoToPerm.repository ==
65 65 Repository.get_by_repo_name(repo))\
66 66 .filter(UserRepoToPerm.user == User.get_by_username(user))\
67 67 .all()
68 68 return perm
69 69
70 70
71 71 @pytest.mark.usefixtures("app")
72 72 class TestAdminRepos(object):
73 73
74 74 def test_repo_list(self, autologin_user, user_util, xhr_header):
75 75 repo = user_util.create_repo()
76 76 repo_name = repo.repo_name
77 77 response = self.app.get(
78 78 route_path('repos_data'), status=200,
79 79 extra_environ=xhr_header)
80 80
81 81 response.mustcontain(repo_name)
82 82
83 83 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
84 84 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
85 85 response = self.app.get(route_path('repo_new'), status=200)
86 86 assert_response = response.assert_response()
87 87 element = assert_response.get_element('[name=repo_type]')
88 88 assert element.get('value') == 'git'
89 89
90 90 def test_create_page_non_restricted_backends(self, autologin_user, backend):
91 91 response = self.app.get(route_path('repo_new'), status=200)
92 92 assert_response = response.assert_response()
93 93 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
94 94
95 95 @pytest.mark.parametrize(
96 96 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
97 97 def test_create(self, autologin_user, backend, suffix, csrf_token):
98 98 repo_name_unicode = backend.new_repo_name(suffix=suffix)
99 99 repo_name = repo_name_unicode.encode('utf8')
100 100 description_unicode = u'description for newly created repo' + suffix
101 101 description = description_unicode.encode('utf8')
102 102 response = self.app.post(
103 103 route_path('repo_create'),
104 104 fixture._get_repo_create_params(
105 105 repo_private=False,
106 106 repo_name=repo_name,
107 107 repo_type=backend.alias,
108 108 repo_description=description,
109 109 csrf_token=csrf_token),
110 110 status=302)
111 111
112 112 self.assert_repository_is_created_correctly(
113 113 repo_name, description, backend)
114 114
115 115 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
116 116 numeric_repo = '1234'
117 117 repo_name = numeric_repo
118 118 description = 'description for newly created repo' + numeric_repo
119 119 self.app.post(
120 120 route_path('repo_create'),
121 121 fixture._get_repo_create_params(
122 122 repo_private=False,
123 123 repo_name=repo_name,
124 124 repo_type=backend.alias,
125 125 repo_description=description,
126 126 csrf_token=csrf_token))
127 127
128 128 self.assert_repository_is_created_correctly(
129 129 repo_name, description, backend)
130 130
131 131 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
132 132 def test_create_in_group(
133 133 self, autologin_user, backend, suffix, csrf_token):
134 134 # create GROUP
135 135 group_name = 'sometest_%s' % backend.alias
136 136 gr = RepoGroupModel().create(group_name=group_name,
137 137 group_description='test',
138 138 owner=TEST_USER_ADMIN_LOGIN)
139 139 Session().commit()
140 140
141 141 repo_name = u'ingroup' + suffix
142 142 repo_name_full = RepoGroup.url_sep().join(
143 143 [group_name, repo_name])
144 144 description = u'description for newly created repo'
145 145 self.app.post(
146 146 route_path('repo_create'),
147 147 fixture._get_repo_create_params(
148 148 repo_private=False,
149 149 repo_name=safe_str(repo_name),
150 150 repo_type=backend.alias,
151 151 repo_description=description,
152 152 repo_group=gr.group_id,
153 153 csrf_token=csrf_token))
154 154
155 155 # TODO: johbo: Cleanup work to fixture
156 156 try:
157 157 self.assert_repository_is_created_correctly(
158 158 repo_name_full, description, backend)
159 159
160 160 new_repo = RepoModel().get_by_repo_name(repo_name_full)
161 161 inherited_perms = UserRepoToPerm.query().filter(
162 162 UserRepoToPerm.repository_id == new_repo.repo_id).all()
163 163 assert len(inherited_perms) == 1
164 164 finally:
165 165 RepoModel().delete(repo_name_full)
166 166 RepoGroupModel().delete(group_name)
167 167 Session().commit()
168 168
169 169 def test_create_in_group_numeric_name(
170 170 self, autologin_user, backend, csrf_token):
171 171 # create GROUP
172 172 group_name = 'sometest_%s' % backend.alias
173 173 gr = RepoGroupModel().create(group_name=group_name,
174 174 group_description='test',
175 175 owner=TEST_USER_ADMIN_LOGIN)
176 176 Session().commit()
177 177
178 178 repo_name = '12345'
179 179 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
180 180 description = 'description for newly created repo'
181 181 self.app.post(
182 182 route_path('repo_create'),
183 183 fixture._get_repo_create_params(
184 184 repo_private=False,
185 185 repo_name=repo_name,
186 186 repo_type=backend.alias,
187 187 repo_description=description,
188 188 repo_group=gr.group_id,
189 189 csrf_token=csrf_token))
190 190
191 191 # TODO: johbo: Cleanup work to fixture
192 192 try:
193 193 self.assert_repository_is_created_correctly(
194 194 repo_name_full, description, backend)
195 195
196 196 new_repo = RepoModel().get_by_repo_name(repo_name_full)
197 197 inherited_perms = UserRepoToPerm.query()\
198 198 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
199 199 assert len(inherited_perms) == 1
200 200 finally:
201 201 RepoModel().delete(repo_name_full)
202 202 RepoGroupModel().delete(group_name)
203 203 Session().commit()
204 204
205 205 def test_create_in_group_without_needed_permissions(self, backend):
206 206 session = login_user_session(
207 207 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
208 208 csrf_token = auth.get_csrf_token(session)
209 209 # revoke
210 210 user_model = UserModel()
211 211 # disable fork and create on default user
212 212 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
213 213 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
214 214 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
215 215 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
216 216
217 217 # disable on regular user
218 218 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
219 219 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
220 220 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
221 221 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
222 222 Session().commit()
223 223
224 224 # create GROUP
225 225 group_name = 'reg_sometest_%s' % backend.alias
226 226 gr = RepoGroupModel().create(group_name=group_name,
227 227 group_description='test',
228 228 owner=TEST_USER_ADMIN_LOGIN)
229 229 Session().commit()
230 230 repo_group_id = gr.group_id
231 231
232 232 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
233 233 gr_allowed = RepoGroupModel().create(
234 234 group_name=group_name_allowed,
235 235 group_description='test',
236 236 owner=TEST_USER_REGULAR_LOGIN)
237 237 allowed_repo_group_id = gr_allowed.group_id
238 238 Session().commit()
239 239
240 240 repo_name = 'ingroup'
241 241 description = 'description for newly created repo'
242 242 response = self.app.post(
243 243 route_path('repo_create'),
244 244 fixture._get_repo_create_params(
245 245 repo_private=False,
246 246 repo_name=repo_name,
247 247 repo_type=backend.alias,
248 248 repo_description=description,
249 249 repo_group=repo_group_id,
250 250 csrf_token=csrf_token))
251 251
252 252 response.mustcontain('Invalid value')
253 253
254 254 # user is allowed to create in this group
255 255 repo_name = 'ingroup'
256 256 repo_name_full = RepoGroup.url_sep().join(
257 257 [group_name_allowed, repo_name])
258 258 description = 'description for newly created repo'
259 259 response = self.app.post(
260 260 route_path('repo_create'),
261 261 fixture._get_repo_create_params(
262 262 repo_private=False,
263 263 repo_name=repo_name,
264 264 repo_type=backend.alias,
265 265 repo_description=description,
266 266 repo_group=allowed_repo_group_id,
267 267 csrf_token=csrf_token))
268 268
269 269 # TODO: johbo: Cleanup in pytest fixture
270 270 try:
271 271 self.assert_repository_is_created_correctly(
272 272 repo_name_full, description, backend)
273 273
274 274 new_repo = RepoModel().get_by_repo_name(repo_name_full)
275 275 inherited_perms = UserRepoToPerm.query().filter(
276 276 UserRepoToPerm.repository_id == new_repo.repo_id).all()
277 277 assert len(inherited_perms) == 1
278 278
279 279 assert repo_on_filesystem(repo_name_full)
280 280 finally:
281 281 RepoModel().delete(repo_name_full)
282 282 RepoGroupModel().delete(group_name)
283 283 RepoGroupModel().delete(group_name_allowed)
284 284 Session().commit()
285 285
286 286 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
287 287 csrf_token):
288 288 # create GROUP
289 289 group_name = 'sometest_%s' % backend.alias
290 290 gr = RepoGroupModel().create(group_name=group_name,
291 291 group_description='test',
292 292 owner=TEST_USER_ADMIN_LOGIN)
293 293 perm = Permission.get_by_key('repository.write')
294 294 RepoGroupModel().grant_user_permission(
295 295 gr, TEST_USER_REGULAR_LOGIN, perm)
296 296
297 297 # add repo permissions
298 298 Session().commit()
299 299 repo_group_id = gr.group_id
300 300 repo_name = 'ingroup_inherited_%s' % backend.alias
301 301 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
302 302 description = 'description for newly created repo'
303 303 self.app.post(
304 304 route_path('repo_create'),
305 305 fixture._get_repo_create_params(
306 306 repo_private=False,
307 307 repo_name=repo_name,
308 308 repo_type=backend.alias,
309 309 repo_description=description,
310 310 repo_group=repo_group_id,
311 311 repo_copy_permissions=True,
312 312 csrf_token=csrf_token))
313 313
314 314 # TODO: johbo: Cleanup to pytest fixture
315 315 try:
316 316 self.assert_repository_is_created_correctly(
317 317 repo_name_full, description, backend)
318 318 except Exception:
319 319 RepoGroupModel().delete(group_name)
320 320 Session().commit()
321 321 raise
322 322
323 323 # check if inherited permissions are applied
324 324 new_repo = RepoModel().get_by_repo_name(repo_name_full)
325 325 inherited_perms = UserRepoToPerm.query().filter(
326 326 UserRepoToPerm.repository_id == new_repo.repo_id).all()
327 327 assert len(inherited_perms) == 2
328 328
329 329 assert TEST_USER_REGULAR_LOGIN in [
330 330 x.user.username for x in inherited_perms]
331 331 assert 'repository.write' in [
332 332 x.permission.permission_name for x in inherited_perms]
333 333
334 334 RepoModel().delete(repo_name_full)
335 335 RepoGroupModel().delete(group_name)
336 336 Session().commit()
337 337
338 338 @pytest.mark.xfail_backends(
339 339 "git", "hg", reason="Missing reposerver support")
340 340 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
341 341 csrf_token):
342 342 source_repo = backend.create_repo(number_of_commits=2)
343 343 source_repo_name = source_repo.repo_name
344 344 reposerver.serve(source_repo.scm_instance())
345 345
346 346 repo_name = backend.new_repo_name()
347 347 response = self.app.post(
348 348 route_path('repo_create'),
349 349 fixture._get_repo_create_params(
350 350 repo_private=False,
351 351 repo_name=repo_name,
352 352 repo_type=backend.alias,
353 353 repo_description='',
354 354 clone_uri=reposerver.url,
355 355 csrf_token=csrf_token),
356 356 status=302)
357 357
358 358 # Should be redirected to the creating page
359 359 response.mustcontain('repo_creating')
360 360
361 361 # Expecting that both repositories have same history
362 362 source_repo = RepoModel().get_by_repo_name(source_repo_name)
363 363 source_vcs = source_repo.scm_instance()
364 364 repo = RepoModel().get_by_repo_name(repo_name)
365 365 repo_vcs = repo.scm_instance()
366 366 assert source_vcs[0].message == repo_vcs[0].message
367 367 assert source_vcs.count() == repo_vcs.count()
368 368 assert source_vcs.commit_ids == repo_vcs.commit_ids
369 369
370 370 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
371 371 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
372 372 csrf_token):
373 373 repo_name = backend.new_repo_name()
374 374 description = 'description for newly created repo'
375 375 response = self.app.post(
376 376 route_path('repo_create'),
377 377 fixture._get_repo_create_params(
378 378 repo_private=False,
379 379 repo_name=repo_name,
380 380 repo_type=backend.alias,
381 381 repo_description=description,
382 382 clone_uri='http://repo.invalid/repo',
383 383 csrf_token=csrf_token))
384 384 response.mustcontain('invalid clone url')
385 385
386 386 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
387 387 def test_create_remote_repo_wrong_clone_uri_hg_svn(
388 388 self, autologin_user, backend, csrf_token):
389 389 repo_name = backend.new_repo_name()
390 390 description = 'description for newly created repo'
391 391 response = self.app.post(
392 392 route_path('repo_create'),
393 393 fixture._get_repo_create_params(
394 394 repo_private=False,
395 395 repo_name=repo_name,
396 396 repo_type=backend.alias,
397 397 repo_description=description,
398 398 clone_uri='svn+http://svn.invalid/repo',
399 399 csrf_token=csrf_token))
400 400 response.mustcontain('invalid clone url')
401 401
402 402 def test_create_with_git_suffix(
403 403 self, autologin_user, backend, csrf_token):
404 404 repo_name = backend.new_repo_name() + ".git"
405 405 description = 'description for newly created repo'
406 406 response = self.app.post(
407 407 route_path('repo_create'),
408 408 fixture._get_repo_create_params(
409 409 repo_private=False,
410 410 repo_name=repo_name,
411 411 repo_type=backend.alias,
412 412 repo_description=description,
413 413 csrf_token=csrf_token))
414 414 response.mustcontain('Repository name cannot end with .git')
415 415
416 416 def test_default_user_cannot_access_private_repo_in_a_group(
417 417 self, autologin_user, user_util, backend):
418 418
419 419 group = user_util.create_repo_group()
420 420
421 421 repo = backend.create_repo(
422 422 repo_private=True, repo_group=group, repo_copy_permissions=True)
423 423
424 424 permissions = _get_permission_for_user(
425 425 user='default', repo=repo.repo_name)
426 426 assert len(permissions) == 1
427 427 assert permissions[0].permission.permission_name == 'repository.none'
428 428 assert permissions[0].repository.private is True
429 429
430 430 def test_create_on_top_level_without_permissions(self, backend):
431 431 session = login_user_session(
432 432 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
433 433 csrf_token = auth.get_csrf_token(session)
434 434
435 435 # revoke
436 436 user_model = UserModel()
437 437 # disable fork and create on default user
438 438 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
439 439 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
440 440 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
441 441 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
442 442
443 443 # disable on regular user
444 444 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
445 445 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
446 446 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
447 447 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
448 448 Session().commit()
449 449
450 450 repo_name = backend.new_repo_name()
451 451 description = 'description for newly created repo'
452 452 response = self.app.post(
453 453 route_path('repo_create'),
454 454 fixture._get_repo_create_params(
455 455 repo_private=False,
456 456 repo_name=repo_name,
457 457 repo_type=backend.alias,
458 458 repo_description=description,
459 459 csrf_token=csrf_token))
460 460
461 461 response.mustcontain(
462 462 u"You do not have the permission to store repositories in "
463 463 u"the root location.")
464 464
465 465 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
466 466 def test_create_repo_when_filesystem_op_fails(
467 467 self, autologin_user, backend, csrf_token):
468 468 repo_name = backend.new_repo_name()
469 469 description = 'description for newly created repo'
470 470
471 471 response = self.app.post(
472 472 route_path('repo_create'),
473 473 fixture._get_repo_create_params(
474 474 repo_private=False,
475 475 repo_name=repo_name,
476 476 repo_type=backend.alias,
477 477 repo_description=description,
478 478 csrf_token=csrf_token))
479 479
480 480 assert_session_flash(
481 481 response, 'Error creating repository %s' % repo_name)
482 482 # repo must not be in db
483 483 assert backend.repo is None
484 484 # repo must not be in filesystem !
485 485 assert not repo_on_filesystem(repo_name)
486 486
487 487 def assert_repository_is_created_correctly(
488 488 self, repo_name, description, backend):
489 489 repo_name_utf8 = safe_str(repo_name)
490 490
491 491 # run the check page that triggers the flash message
492 492 response = self.app.get(
493 493 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
494 494 assert response.json == {u'result': True}
495 495
496 496 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
497 urllib.quote(repo_name_utf8), repo_name)
497 urllib.parse.quote(repo_name_utf8), repo_name)
498 498 assert_session_flash(response, flash_msg)
499 499
500 500 # test if the repo was created in the database
501 501 new_repo = RepoModel().get_by_repo_name(repo_name)
502 502
503 503 assert new_repo.repo_name == repo_name
504 504 assert new_repo.description == description
505 505
506 506 # test if the repository is visible in the list ?
507 507 response = self.app.get(
508 508 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
509 509 response.mustcontain(repo_name)
510 510 response.mustcontain(backend.alias)
511 511
512 512 assert repo_on_filesystem(repo_name)
@@ -1,194 +1,194 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import os
22 22 import pytest
23 23
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.repo_group import RepoGroupModel
29 29 from rhodecode.tests import (
30 30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
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
37 import urllib.request, urllib.parse, urllib.error
38 38
39 39 base_url = {
40 40 'repo_groups': ADMIN_PREFIX + '/repo_groups',
41 41 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
42 42 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
43 43 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
44 44
45 45 }[name].format(**kwargs)
46 46
47 47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 49 return base_url
50 50
51 51
52 52 def _get_permission_for_user(user, repo):
53 53 perm = UserRepoToPerm.query()\
54 54 .filter(UserRepoToPerm.repository ==
55 55 Repository.get_by_repo_name(repo))\
56 56 .filter(UserRepoToPerm.user == User.get_by_username(user))\
57 57 .all()
58 58 return perm
59 59
60 60
61 61 @pytest.mark.usefixtures("app")
62 62 class TestAdminRepositoryGroups(object):
63 63
64 64 def test_show_repo_groups(self, autologin_user):
65 65 self.app.get(route_path('repo_groups'))
66 66
67 67 def test_show_repo_groups_data(self, autologin_user, xhr_header):
68 68 response = self.app.get(route_path(
69 69 'repo_groups_data'), extra_environ=xhr_header)
70 70
71 71 all_repo_groups = RepoGroup.query().count()
72 72 assert response.json['recordsTotal'] == all_repo_groups
73 73
74 74 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
75 75 response = self.app.get(route_path(
76 76 'repo_groups_data', params={'search[value]': 'empty_search'}),
77 77 extra_environ=xhr_header)
78 78
79 79 all_repo_groups = RepoGroup.query().count()
80 80 assert response.json['recordsTotal'] == all_repo_groups
81 81 assert response.json['recordsFiltered'] == 0
82 82
83 83 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
84 84 fixture.create_repo_group('test_repo_group')
85 85 response = self.app.get(route_path(
86 86 'repo_groups_data'), extra_environ=xhr_header)
87 87 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
88 88 fixture.destroy_repo_group('test_repo_group')
89 89
90 90 def test_new(self, autologin_user):
91 91 self.app.get(route_path('repo_group_new'))
92 92
93 93 def test_new_with_parent_group(self, autologin_user, user_util):
94 94 gr = user_util.create_repo_group()
95 95
96 96 self.app.get(route_path('repo_group_new'),
97 97 params=dict(parent_group=gr.group_name))
98 98
99 99 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
100 100 self.app.get(route_path('repo_group_new'), status=403)
101 101
102 102 @pytest.mark.parametrize('repo_group_name', [
103 103 'git_repo',
104 104 'git_repo_ąć',
105 105 'hg_repo',
106 106 '12345',
107 107 'hg_repo_ąć',
108 108 ])
109 109 def test_create(self, autologin_user, repo_group_name, csrf_token):
110 110 repo_group_name_unicode = repo_group_name.decode('utf8')
111 111 description = 'description for newly created repo group'
112 112
113 113 response = self.app.post(
114 114 route_path('repo_group_create'),
115 115 fixture._get_group_create_params(
116 116 group_name=repo_group_name,
117 117 group_description=description,
118 118 csrf_token=csrf_token))
119 119
120 120 # run the check page that triggers the flash message
121 121 repo_gr_url = h.route_path(
122 122 'repo_group_home', repo_group_name=repo_group_name)
123 123
124 124 assert_session_flash(
125 125 response,
126 126 'Created repository group <a href="%s">%s</a>' % (
127 127 repo_gr_url, repo_group_name_unicode))
128 128
129 129 # # test if the repo group was created in the database
130 130 new_repo_group = RepoGroupModel()._get_repo_group(
131 131 repo_group_name_unicode)
132 132 assert new_repo_group is not None
133 133
134 134 assert new_repo_group.group_name == repo_group_name_unicode
135 135 assert new_repo_group.group_description == description
136 136
137 137 # test if the repository is visible in the list ?
138 138 response = self.app.get(repo_gr_url)
139 139 response.mustcontain(repo_group_name)
140 140
141 141 # test if the repository group was created on filesystem
142 142 is_on_filesystem = os.path.isdir(
143 143 os.path.join(TESTS_TMP_PATH, repo_group_name))
144 144 if not is_on_filesystem:
145 145 self.fail('no repo group %s in filesystem' % repo_group_name)
146 146
147 147 RepoGroupModel().delete(repo_group_name_unicode)
148 148 Session().commit()
149 149
150 150 @pytest.mark.parametrize('repo_group_name', [
151 151 'git_repo',
152 152 'git_repo_ąć',
153 153 'hg_repo',
154 154 '12345',
155 155 'hg_repo_ąć',
156 156 ])
157 157 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
158 158 parent_group = user_util.create_repo_group()
159 159 parent_group_name = parent_group.group_name
160 160
161 161 expected_group_name = '{}/{}'.format(
162 162 parent_group_name, repo_group_name)
163 163 expected_group_name_unicode = expected_group_name.decode('utf8')
164 164
165 165 try:
166 166 response = self.app.post(
167 167 route_path('repo_group_create'),
168 168 fixture._get_group_create_params(
169 169 group_name=repo_group_name,
170 170 group_parent_id=parent_group.group_id,
171 171 group_description='Test desciption',
172 172 csrf_token=csrf_token))
173 173
174 174 assert_session_flash(
175 175 response,
176 176 u'Created repository group <a href="%s">%s</a>' % (
177 177 h.route_path('repo_group_home',
178 178 repo_group_name=expected_group_name),
179 179 expected_group_name_unicode))
180 180 finally:
181 181 RepoGroupModel().delete(expected_group_name_unicode)
182 182 Session().commit()
183 183
184 184 def test_user_with_creation_permissions_cannot_create_subgroups(
185 185 self, autologin_regular_user, user_util):
186 186
187 187 user_util.grant_user_permission(
188 188 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
189 189 parent_group = user_util.create_repo_group()
190 190 parent_group_id = parent_group.group_id
191 191 self.app.get(
192 192 route_path('repo_group_new',
193 193 params=dict(parent_group=parent_group_id), ),
194 194 status=403)
@@ -1,767 +1,767 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import mock
22 22 import pytest
23 23
24 24 import rhodecode
25 25 from rhodecode.apps._base import ADMIN_PREFIX
26 26 from rhodecode.lib.utils2 import md5
27 27 from rhodecode.model.db import RhodeCodeUi
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 30 from rhodecode.tests import assert_session_flash
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33
34 34 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39 39 from rhodecode.apps._base import ADMIN_PREFIX
40 40
41 41 base_url = {
42 42
43 43 'admin_settings':
44 44 ADMIN_PREFIX +'/settings',
45 45 'admin_settings_update':
46 46 ADMIN_PREFIX + '/settings/update',
47 47 'admin_settings_global':
48 48 ADMIN_PREFIX + '/settings/global',
49 49 'admin_settings_global_update':
50 50 ADMIN_PREFIX + '/settings/global/update',
51 51 'admin_settings_vcs':
52 52 ADMIN_PREFIX + '/settings/vcs',
53 53 'admin_settings_vcs_update':
54 54 ADMIN_PREFIX + '/settings/vcs/update',
55 55 'admin_settings_vcs_svn_pattern_delete':
56 56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
57 57 'admin_settings_mapping':
58 58 ADMIN_PREFIX + '/settings/mapping',
59 59 'admin_settings_mapping_update':
60 60 ADMIN_PREFIX + '/settings/mapping/update',
61 61 'admin_settings_visual':
62 62 ADMIN_PREFIX + '/settings/visual',
63 63 'admin_settings_visual_update':
64 64 ADMIN_PREFIX + '/settings/visual/update',
65 65 'admin_settings_issuetracker':
66 66 ADMIN_PREFIX + '/settings/issue-tracker',
67 67 'admin_settings_issuetracker_update':
68 68 ADMIN_PREFIX + '/settings/issue-tracker/update',
69 69 'admin_settings_issuetracker_test':
70 70 ADMIN_PREFIX + '/settings/issue-tracker/test',
71 71 'admin_settings_issuetracker_delete':
72 72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
73 73 'admin_settings_email':
74 74 ADMIN_PREFIX + '/settings/email',
75 75 'admin_settings_email_update':
76 76 ADMIN_PREFIX + '/settings/email/update',
77 77 'admin_settings_hooks':
78 78 ADMIN_PREFIX + '/settings/hooks',
79 79 'admin_settings_hooks_update':
80 80 ADMIN_PREFIX + '/settings/hooks/update',
81 81 'admin_settings_hooks_delete':
82 82 ADMIN_PREFIX + '/settings/hooks/delete',
83 83 'admin_settings_search':
84 84 ADMIN_PREFIX + '/settings/search',
85 85 'admin_settings_labs':
86 86 ADMIN_PREFIX + '/settings/labs',
87 87 'admin_settings_labs_update':
88 88 ADMIN_PREFIX + '/settings/labs/update',
89 89
90 90 'admin_settings_sessions':
91 91 ADMIN_PREFIX + '/settings/sessions',
92 92 'admin_settings_sessions_cleanup':
93 93 ADMIN_PREFIX + '/settings/sessions/cleanup',
94 94 'admin_settings_system':
95 95 ADMIN_PREFIX + '/settings/system',
96 96 'admin_settings_system_update':
97 97 ADMIN_PREFIX + '/settings/system/updates',
98 98 'admin_settings_open_source':
99 99 ADMIN_PREFIX + '/settings/open_source',
100 100
101 101
102 102 }[name].format(**kwargs)
103 103
104 104 if params:
105 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
105 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
106 106 return base_url
107 107
108 108
109 109 @pytest.mark.usefixtures('autologin_user', 'app')
110 110 class TestAdminSettingsController(object):
111 111
112 112 @pytest.mark.parametrize('urlname', [
113 113 'admin_settings_vcs',
114 114 'admin_settings_mapping',
115 115 'admin_settings_global',
116 116 'admin_settings_visual',
117 117 'admin_settings_email',
118 118 'admin_settings_hooks',
119 119 'admin_settings_search',
120 120 ])
121 121 def test_simple_get(self, urlname):
122 122 self.app.get(route_path(urlname))
123 123
124 124 def test_create_custom_hook(self, csrf_token):
125 125 response = self.app.post(
126 126 route_path('admin_settings_hooks_update'),
127 127 params={
128 128 'new_hook_ui_key': 'test_hooks_1',
129 129 'new_hook_ui_value': 'cd /tmp',
130 130 'csrf_token': csrf_token})
131 131
132 132 response = response.follow()
133 133 response.mustcontain('test_hooks_1')
134 134 response.mustcontain('cd /tmp')
135 135
136 136 def test_create_custom_hook_delete(self, csrf_token):
137 137 response = self.app.post(
138 138 route_path('admin_settings_hooks_update'),
139 139 params={
140 140 'new_hook_ui_key': 'test_hooks_2',
141 141 'new_hook_ui_value': 'cd /tmp2',
142 142 'csrf_token': csrf_token})
143 143
144 144 response = response.follow()
145 145 response.mustcontain('test_hooks_2')
146 146 response.mustcontain('cd /tmp2')
147 147
148 148 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149 149
150 150 # delete
151 151 self.app.post(
152 152 route_path('admin_settings_hooks_delete'),
153 153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 154 response = self.app.get(route_path('admin_settings_hooks'))
155 155 response.mustcontain(no=['test_hooks_2'])
156 156 response.mustcontain(no=['cd /tmp2'])
157 157
158 158
159 159 @pytest.mark.usefixtures('autologin_user', 'app')
160 160 class TestAdminSettingsGlobal(object):
161 161
162 162 def test_pre_post_code_code_active(self, csrf_token):
163 163 pre_code = 'rc-pre-code-187652122'
164 164 post_code = 'rc-postcode-98165231'
165 165
166 166 response = self.post_and_verify_settings({
167 167 'rhodecode_pre_code': pre_code,
168 168 'rhodecode_post_code': post_code,
169 169 'csrf_token': csrf_token,
170 170 })
171 171
172 172 response = response.follow()
173 173 response.mustcontain(pre_code, post_code)
174 174
175 175 def test_pre_post_code_code_inactive(self, csrf_token):
176 176 pre_code = 'rc-pre-code-187652122'
177 177 post_code = 'rc-postcode-98165231'
178 178 response = self.post_and_verify_settings({
179 179 'rhodecode_pre_code': '',
180 180 'rhodecode_post_code': '',
181 181 'csrf_token': csrf_token,
182 182 })
183 183
184 184 response = response.follow()
185 185 response.mustcontain(no=[pre_code, post_code])
186 186
187 187 def test_captcha_activate(self, csrf_token):
188 188 self.post_and_verify_settings({
189 189 'rhodecode_captcha_private_key': '1234567890',
190 190 'rhodecode_captcha_public_key': '1234567890',
191 191 'csrf_token': csrf_token,
192 192 })
193 193
194 194 response = self.app.get(ADMIN_PREFIX + '/register')
195 195 response.mustcontain('captcha')
196 196
197 197 def test_captcha_deactivate(self, csrf_token):
198 198 self.post_and_verify_settings({
199 199 'rhodecode_captcha_private_key': '',
200 200 'rhodecode_captcha_public_key': '1234567890',
201 201 'csrf_token': csrf_token,
202 202 })
203 203
204 204 response = self.app.get(ADMIN_PREFIX + '/register')
205 205 response.mustcontain(no=['captcha'])
206 206
207 207 def test_title_change(self, csrf_token):
208 208 old_title = 'RhodeCode'
209 209
210 210 for new_title in ['Changed', 'Żółwik', old_title]:
211 211 response = self.post_and_verify_settings({
212 212 'rhodecode_title': new_title,
213 213 'csrf_token': csrf_token,
214 214 })
215 215
216 216 response = response.follow()
217 217 response.mustcontain(new_title)
218 218
219 219 def post_and_verify_settings(self, settings):
220 220 old_title = 'RhodeCode'
221 221 old_realm = 'RhodeCode authentication'
222 222 params = {
223 223 'rhodecode_title': old_title,
224 224 'rhodecode_realm': old_realm,
225 225 'rhodecode_pre_code': '',
226 226 'rhodecode_post_code': '',
227 227 'rhodecode_captcha_private_key': '',
228 228 'rhodecode_captcha_public_key': '',
229 229 'rhodecode_create_personal_repo_group': False,
230 230 'rhodecode_personal_repo_group_pattern': '${username}',
231 231 }
232 232 params.update(settings)
233 233 response = self.app.post(
234 234 route_path('admin_settings_global_update'), params=params)
235 235
236 236 assert_session_flash(response, 'Updated application settings')
237 237 app_settings = SettingsModel().get_all_settings()
238 238 del settings['csrf_token']
239 239 for key, value in settings.iteritems():
240 240 assert app_settings[key] == value.decode('utf-8')
241 241
242 242 return response
243 243
244 244
245 245 @pytest.mark.usefixtures('autologin_user', 'app')
246 246 class TestAdminSettingsVcs(object):
247 247
248 248 def test_contains_svn_default_patterns(self):
249 249 response = self.app.get(route_path('admin_settings_vcs'))
250 250 expected_patterns = [
251 251 '/trunk',
252 252 '/branches/*',
253 253 '/tags/*',
254 254 ]
255 255 for pattern in expected_patterns:
256 256 response.mustcontain(pattern)
257 257
258 258 def test_add_new_svn_branch_and_tag_pattern(
259 259 self, backend_svn, form_defaults, disable_sql_cache,
260 260 csrf_token):
261 261 form_defaults.update({
262 262 'new_svn_branch': '/exp/branches/*',
263 263 'new_svn_tag': '/important_tags/*',
264 264 'csrf_token': csrf_token,
265 265 })
266 266
267 267 response = self.app.post(
268 268 route_path('admin_settings_vcs_update'),
269 269 params=form_defaults, status=302)
270 270 response = response.follow()
271 271
272 272 # Expect to find the new values on the page
273 273 response.mustcontain('/exp/branches/*')
274 274 response.mustcontain('/important_tags/*')
275 275
276 276 # Expect that those patterns are used to match branches and tags now
277 277 repo = backend_svn['svn-simple-layout'].scm_instance()
278 278 assert 'exp/branches/exp-sphinx-docs' in repo.branches
279 279 assert 'important_tags/v0.5' in repo.tags
280 280
281 281 def test_add_same_svn_value_twice_shows_an_error_message(
282 282 self, form_defaults, csrf_token, settings_util):
283 283 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
284 284 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
285 285
286 286 response = self.app.post(
287 287 route_path('admin_settings_vcs_update'),
288 288 params={
289 289 'paths_root_path': form_defaults['paths_root_path'],
290 290 'new_svn_branch': '/test',
291 291 'new_svn_tag': '/test',
292 292 'csrf_token': csrf_token,
293 293 },
294 294 status=200)
295 295
296 296 response.mustcontain("Pattern already exists")
297 297 response.mustcontain("Some form inputs contain invalid data.")
298 298
299 299 @pytest.mark.parametrize('section', [
300 300 'vcs_svn_branch',
301 301 'vcs_svn_tag',
302 302 ])
303 303 def test_delete_svn_patterns(
304 304 self, section, csrf_token, settings_util):
305 305 setting = settings_util.create_rhodecode_ui(
306 306 section, '/test_delete', cleanup=False)
307 307
308 308 self.app.post(
309 309 route_path('admin_settings_vcs_svn_pattern_delete'),
310 310 params={
311 311 'delete_svn_pattern': setting.ui_id,
312 312 'csrf_token': csrf_token},
313 313 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
314 314
315 315 @pytest.mark.parametrize('section', [
316 316 'vcs_svn_branch',
317 317 'vcs_svn_tag',
318 318 ])
319 319 def test_delete_svn_patterns_raises_404_when_no_xhr(
320 320 self, section, csrf_token, settings_util):
321 321 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
322 322
323 323 self.app.post(
324 324 route_path('admin_settings_vcs_svn_pattern_delete'),
325 325 params={
326 326 'delete_svn_pattern': setting.ui_id,
327 327 'csrf_token': csrf_token},
328 328 status=404)
329 329
330 330 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
331 331 form_defaults.update({
332 332 'csrf_token': csrf_token,
333 333 'extensions_hgsubversion': 'True',
334 334 })
335 335 response = self.app.post(
336 336 route_path('admin_settings_vcs_update'),
337 337 params=form_defaults,
338 338 status=302)
339 339
340 340 response = response.follow()
341 341 extensions_input = (
342 342 '<input id="extensions_hgsubversion" '
343 343 'name="extensions_hgsubversion" type="checkbox" '
344 344 'value="True" checked="checked" />')
345 345 response.mustcontain(extensions_input)
346 346
347 347 def test_extensions_hgevolve(self, form_defaults, csrf_token):
348 348 form_defaults.update({
349 349 'csrf_token': csrf_token,
350 350 'extensions_evolve': 'True',
351 351 })
352 352 response = self.app.post(
353 353 route_path('admin_settings_vcs_update'),
354 354 params=form_defaults,
355 355 status=302)
356 356
357 357 response = response.follow()
358 358 extensions_input = (
359 359 '<input id="extensions_evolve" '
360 360 'name="extensions_evolve" type="checkbox" '
361 361 'value="True" checked="checked" />')
362 362 response.mustcontain(extensions_input)
363 363
364 364 def test_has_a_section_for_pull_request_settings(self):
365 365 response = self.app.get(route_path('admin_settings_vcs'))
366 366 response.mustcontain('Pull Request Settings')
367 367
368 368 def test_has_an_input_for_invalidation_of_inline_comments(self):
369 369 response = self.app.get(route_path('admin_settings_vcs'))
370 370 assert_response = response.assert_response()
371 371 assert_response.one_element_exists(
372 372 '[name=rhodecode_use_outdated_comments]')
373 373
374 374 @pytest.mark.parametrize('new_value', [True, False])
375 375 def test_allows_to_change_invalidation_of_inline_comments(
376 376 self, form_defaults, csrf_token, new_value):
377 377 setting_key = 'use_outdated_comments'
378 378 setting = SettingsModel().create_or_update_setting(
379 379 setting_key, not new_value, 'bool')
380 380 Session().add(setting)
381 381 Session().commit()
382 382
383 383 form_defaults.update({
384 384 'csrf_token': csrf_token,
385 385 'rhodecode_use_outdated_comments': str(new_value),
386 386 })
387 387 response = self.app.post(
388 388 route_path('admin_settings_vcs_update'),
389 389 params=form_defaults,
390 390 status=302)
391 391 response = response.follow()
392 392 setting = SettingsModel().get_setting_by_name(setting_key)
393 393 assert setting.app_settings_value is new_value
394 394
395 395 @pytest.mark.parametrize('new_value', [True, False])
396 396 def test_allows_to_change_hg_rebase_merge_strategy(
397 397 self, form_defaults, csrf_token, new_value):
398 398 setting_key = 'hg_use_rebase_for_merging'
399 399
400 400 form_defaults.update({
401 401 'csrf_token': csrf_token,
402 402 'rhodecode_' + setting_key: str(new_value),
403 403 })
404 404
405 405 with mock.patch.dict(
406 406 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
407 407 self.app.post(
408 408 route_path('admin_settings_vcs_update'),
409 409 params=form_defaults,
410 410 status=302)
411 411
412 412 setting = SettingsModel().get_setting_by_name(setting_key)
413 413 assert setting.app_settings_value is new_value
414 414
415 415 @pytest.fixture()
416 416 def disable_sql_cache(self, request):
417 417 patcher = mock.patch(
418 418 'rhodecode.lib.caching_query.FromCache.process_query')
419 419 request.addfinalizer(patcher.stop)
420 420 patcher.start()
421 421
422 422 @pytest.fixture()
423 423 def form_defaults(self):
424 424 from rhodecode.apps.admin.views.settings import AdminSettingsView
425 425 return AdminSettingsView._form_defaults()
426 426
427 427 # TODO: johbo: What we really want is to checkpoint before a test run and
428 428 # reset the session afterwards.
429 429 @pytest.fixture(scope='class', autouse=True)
430 430 def cleanup_settings(self, request, baseapp):
431 431 ui_id = RhodeCodeUi.ui_id
432 432 original_ids = list(
433 433 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
434 434
435 435 @request.addfinalizer
436 436 def cleanup():
437 437 RhodeCodeUi.query().filter(
438 438 ui_id.notin_(original_ids)).delete(False)
439 439
440 440
441 441 @pytest.mark.usefixtures('autologin_user', 'app')
442 442 class TestLabsSettings(object):
443 443 def test_get_settings_page_disabled(self):
444 444 with mock.patch.dict(
445 445 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
446 446
447 447 response = self.app.get(
448 448 route_path('admin_settings_labs'), status=302)
449 449
450 450 assert response.location.endswith(route_path('admin_settings'))
451 451
452 452 def test_get_settings_page_enabled(self):
453 453 from rhodecode.apps.admin.views import settings
454 454 lab_settings = [
455 455 settings.LabSetting(
456 456 key='rhodecode_bool',
457 457 type='bool',
458 458 group='bool group',
459 459 label='bool label',
460 460 help='bool help'
461 461 ),
462 462 settings.LabSetting(
463 463 key='rhodecode_text',
464 464 type='unicode',
465 465 group='text group',
466 466 label='text label',
467 467 help='text help'
468 468 ),
469 469 ]
470 470 with mock.patch.dict(rhodecode.CONFIG,
471 471 {'labs_settings_active': 'true'}):
472 472 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
473 473 response = self.app.get(route_path('admin_settings_labs'))
474 474
475 475 assert '<label>bool group:</label>' in response
476 476 assert '<label for="rhodecode_bool">bool label</label>' in response
477 477 assert '<p class="help-block">bool help</p>' in response
478 478 assert 'name="rhodecode_bool" type="checkbox"' in response
479 479
480 480 assert '<label>text group:</label>' in response
481 481 assert '<label for="rhodecode_text">text label</label>' in response
482 482 assert '<p class="help-block">text help</p>' in response
483 483 assert 'name="rhodecode_text" size="60" type="text"' in response
484 484
485 485
486 486 @pytest.mark.usefixtures('app')
487 487 class TestOpenSourceLicenses(object):
488 488
489 489 def test_records_are_displayed(self, autologin_user):
490 490 sample_licenses = [
491 491 {
492 492 "license": [
493 493 {
494 494 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
495 495 "shortName": "bsdOriginal",
496 496 "spdxId": "BSD-4-Clause",
497 497 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
498 498 }
499 499 ],
500 500 "name": "python2.7-coverage-3.7.1"
501 501 },
502 502 {
503 503 "license": [
504 504 {
505 505 "fullName": "MIT License",
506 506 "shortName": "mit",
507 507 "spdxId": "MIT",
508 508 "url": "http://spdx.org/licenses/MIT.html"
509 509 }
510 510 ],
511 511 "name": "python2.7-bootstrapped-pip-9.0.1"
512 512 },
513 513 ]
514 514 read_licenses_patch = mock.patch(
515 515 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
516 516 return_value=sample_licenses)
517 517 with read_licenses_patch:
518 518 response = self.app.get(
519 519 route_path('admin_settings_open_source'), status=200)
520 520
521 521 assert_response = response.assert_response()
522 522 assert_response.element_contains(
523 523 '.panel-heading', 'Licenses of Third Party Packages')
524 524 for license_data in sample_licenses:
525 525 response.mustcontain(license_data["license"][0]["spdxId"])
526 526 assert_response.element_contains('.panel-body', license_data["name"])
527 527
528 528 def test_records_can_be_read(self, autologin_user):
529 529 response = self.app.get(
530 530 route_path('admin_settings_open_source'), status=200)
531 531 assert_response = response.assert_response()
532 532 assert_response.element_contains(
533 533 '.panel-heading', 'Licenses of Third Party Packages')
534 534
535 535 def test_forbidden_when_normal_user(self, autologin_regular_user):
536 536 self.app.get(
537 537 route_path('admin_settings_open_source'), status=404)
538 538
539 539
540 540 @pytest.mark.usefixtures('app')
541 541 class TestUserSessions(object):
542 542
543 543 def test_forbidden_when_normal_user(self, autologin_regular_user):
544 544 self.app.get(route_path('admin_settings_sessions'), status=404)
545 545
546 546 def test_show_sessions_page(self, autologin_user):
547 547 response = self.app.get(route_path('admin_settings_sessions'), status=200)
548 548 response.mustcontain('file')
549 549
550 550 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
551 551
552 552 post_data = {
553 553 'csrf_token': csrf_token,
554 554 'expire_days': '60'
555 555 }
556 556 response = self.app.post(
557 557 route_path('admin_settings_sessions_cleanup'), params=post_data,
558 558 status=302)
559 559 assert_session_flash(response, 'Cleaned up old sessions')
560 560
561 561
562 562 @pytest.mark.usefixtures('app')
563 563 class TestAdminSystemInfo(object):
564 564
565 565 def test_forbidden_when_normal_user(self, autologin_regular_user):
566 566 self.app.get(route_path('admin_settings_system'), status=404)
567 567
568 568 def test_system_info_page(self, autologin_user):
569 569 response = self.app.get(route_path('admin_settings_system'))
570 570 response.mustcontain('RhodeCode Community Edition, version {}'.format(
571 571 rhodecode.__version__))
572 572
573 573 def test_system_update_new_version(self, autologin_user):
574 574 update_data = {
575 575 'versions': [
576 576 {
577 577 'version': '100.3.1415926535',
578 578 'general': 'The latest version we are ever going to ship'
579 579 },
580 580 {
581 581 'version': '0.0.0',
582 582 'general': 'The first version we ever shipped'
583 583 }
584 584 ]
585 585 }
586 586 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
587 587 response = self.app.get(route_path('admin_settings_system_update'))
588 588 response.mustcontain('A <b>new version</b> is available')
589 589
590 590 def test_system_update_nothing_new(self, autologin_user):
591 591 update_data = {
592 592 'versions': [
593 593 {
594 594 'version': '0.0.0',
595 595 'general': 'The first version we ever shipped'
596 596 }
597 597 ]
598 598 }
599 599 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
600 600 response = self.app.get(route_path('admin_settings_system_update'))
601 601 response.mustcontain(
602 602 'This instance is already running the <b>latest</b> stable version')
603 603
604 604 def test_system_update_bad_response(self, autologin_user):
605 605 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
606 606 response = self.app.get(route_path('admin_settings_system_update'))
607 607 response.mustcontain(
608 608 'Bad data sent from update server')
609 609
610 610
611 611 @pytest.mark.usefixtures("app")
612 612 class TestAdminSettingsIssueTracker(object):
613 613 RC_PREFIX = 'rhodecode_'
614 614 SHORT_PATTERN_KEY = 'issuetracker_pat_'
615 615 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
616 616 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
617 617
618 618 def test_issuetracker_index(self, autologin_user):
619 619 response = self.app.get(route_path('admin_settings_issuetracker'))
620 620 assert response.status_code == 200
621 621
622 622 def test_add_empty_issuetracker_pattern(
623 623 self, request, autologin_user, csrf_token):
624 624 post_url = route_path('admin_settings_issuetracker_update')
625 625 post_data = {
626 626 'csrf_token': csrf_token
627 627 }
628 628 self.app.post(post_url, post_data, status=302)
629 629
630 630 def test_add_issuetracker_pattern(
631 631 self, request, autologin_user, csrf_token):
632 632 pattern = 'issuetracker_pat'
633 633 another_pattern = pattern+'1'
634 634 post_url = route_path('admin_settings_issuetracker_update')
635 635 post_data = {
636 636 'new_pattern_pattern_0': pattern,
637 637 'new_pattern_url_0': 'http://url',
638 638 'new_pattern_prefix_0': 'prefix',
639 639 'new_pattern_description_0': 'description',
640 640 'new_pattern_pattern_1': another_pattern,
641 641 'new_pattern_url_1': 'https://url1',
642 642 'new_pattern_prefix_1': 'prefix1',
643 643 'new_pattern_description_1': 'description1',
644 644 'csrf_token': csrf_token
645 645 }
646 646 self.app.post(post_url, post_data, status=302)
647 647 settings = SettingsModel().get_all_settings()
648 648 self.uid = md5(pattern)
649 649 assert settings[self.PATTERN_KEY+self.uid] == pattern
650 650 self.another_uid = md5(another_pattern)
651 651 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
652 652
653 653 @request.addfinalizer
654 654 def cleanup():
655 655 defaults = SettingsModel().get_all_settings()
656 656
657 657 entries = [name for name in defaults if (
658 658 (self.uid in name) or (self.another_uid) in name)]
659 659 start = len(self.RC_PREFIX)
660 660 for del_key in entries:
661 661 # TODO: anderson: get_by_name needs name without prefix
662 662 entry = SettingsModel().get_setting_by_name(del_key[start:])
663 663 Session().delete(entry)
664 664
665 665 Session().commit()
666 666
667 667 def test_edit_issuetracker_pattern(
668 668 self, autologin_user, backend, csrf_token, request):
669 669
670 670 old_pattern = 'issuetracker_pat1'
671 671 old_uid = md5(old_pattern)
672 672
673 673 post_url = route_path('admin_settings_issuetracker_update')
674 674 post_data = {
675 675 'new_pattern_pattern_0': old_pattern,
676 676 'new_pattern_url_0': 'http://url',
677 677 'new_pattern_prefix_0': 'prefix',
678 678 'new_pattern_description_0': 'description',
679 679
680 680 'csrf_token': csrf_token
681 681 }
682 682 self.app.post(post_url, post_data, status=302)
683 683
684 684 new_pattern = 'issuetracker_pat1_edited'
685 685 self.new_uid = md5(new_pattern)
686 686
687 687 post_url = route_path('admin_settings_issuetracker_update')
688 688 post_data = {
689 689 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
690 690 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
691 691 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
692 692 'new_pattern_description_{}'.format(old_uid): 'description_edited',
693 693 'uid': old_uid,
694 694 'csrf_token': csrf_token
695 695 }
696 696 self.app.post(post_url, post_data, status=302)
697 697
698 698 settings = SettingsModel().get_all_settings()
699 699 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
700 700 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
701 701 assert self.PATTERN_KEY+old_uid not in settings
702 702
703 703 @request.addfinalizer
704 704 def cleanup():
705 705 IssueTrackerSettingsModel().delete_entries(old_uid)
706 706 IssueTrackerSettingsModel().delete_entries(self.new_uid)
707 707
708 708 def test_replace_issuetracker_pattern_description(
709 709 self, autologin_user, csrf_token, request, settings_util):
710 710 prefix = 'issuetracker'
711 711 pattern = 'issuetracker_pat'
712 712 self.uid = md5(pattern)
713 713 pattern_key = '_'.join([prefix, 'pat', self.uid])
714 714 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
715 715 desc_key = '_'.join([prefix, 'desc', self.uid])
716 716 rc_desc_key = '_'.join(['rhodecode', desc_key])
717 717 new_description = 'new_description'
718 718
719 719 settings_util.create_rhodecode_setting(
720 720 pattern_key, pattern, 'unicode', cleanup=False)
721 721 settings_util.create_rhodecode_setting(
722 722 desc_key, 'old description', 'unicode', cleanup=False)
723 723
724 724 post_url = route_path('admin_settings_issuetracker_update')
725 725 post_data = {
726 726 'new_pattern_pattern_0': pattern,
727 727 'new_pattern_url_0': 'https://url',
728 728 'new_pattern_prefix_0': 'prefix',
729 729 'new_pattern_description_0': new_description,
730 730 'uid': self.uid,
731 731 'csrf_token': csrf_token
732 732 }
733 733 self.app.post(post_url, post_data, status=302)
734 734 settings = SettingsModel().get_all_settings()
735 735 assert settings[rc_pattern_key] == pattern
736 736 assert settings[rc_desc_key] == new_description
737 737
738 738 @request.addfinalizer
739 739 def cleanup():
740 740 IssueTrackerSettingsModel().delete_entries(self.uid)
741 741
742 742 def test_delete_issuetracker_pattern(
743 743 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
744 744
745 745 old_pattern = 'issuetracker_pat_deleted'
746 746 old_uid = md5(old_pattern)
747 747
748 748 post_url = route_path('admin_settings_issuetracker_update')
749 749 post_data = {
750 750 'new_pattern_pattern_0': old_pattern,
751 751 'new_pattern_url_0': 'http://url',
752 752 'new_pattern_prefix_0': 'prefix',
753 753 'new_pattern_description_0': 'description',
754 754
755 755 'csrf_token': csrf_token
756 756 }
757 757 self.app.post(post_url, post_data, status=302)
758 758
759 759 post_url = route_path('admin_settings_issuetracker_delete')
760 760 post_data = {
761 761 'uid': old_uid,
762 762 'csrf_token': csrf_token
763 763 }
764 764 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
765 765 settings = SettingsModel().get_all_settings()
766 766 assert self.PATTERN_KEY+old_uid not in settings
767 767 assert self.DESC_KEY + old_uid not in settings
@@ -1,170 +1,170 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.model.db import UserGroup, User
24 24 from rhodecode.model.meta import Session
25 25
26 26 from rhodecode.tests import (
27 27 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 28 from rhodecode.tests.fixture import Fixture
29 29
30 30 fixture = Fixture()
31 31
32 32
33 33 def route_path(name, params=None, **kwargs):
34 import urllib
34 import urllib.request, urllib.parse, urllib.error
35 35 from rhodecode.apps._base import ADMIN_PREFIX
36 36
37 37 base_url = {
38 38 'user_groups': ADMIN_PREFIX + '/user_groups',
39 39 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
40 40 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
41 41 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
42 42 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
43 43 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
44 44 }[name].format(**kwargs)
45 45
46 46 if params:
47 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 48 return base_url
49 49
50 50
51 51 class TestAdminUserGroupsView(TestController):
52 52
53 53 def test_show_users(self):
54 54 self.log_user()
55 55 self.app.get(route_path('user_groups'))
56 56
57 57 def test_show_user_groups_data(self, xhr_header):
58 58 self.log_user()
59 59 response = self.app.get(route_path(
60 60 'user_groups_data'), extra_environ=xhr_header)
61 61
62 62 all_user_groups = UserGroup.query().count()
63 63 assert response.json['recordsTotal'] == all_user_groups
64 64
65 65 def test_show_user_groups_data_filtered(self, xhr_header):
66 66 self.log_user()
67 67 response = self.app.get(route_path(
68 68 'user_groups_data', params={'search[value]': 'empty_search'}),
69 69 extra_environ=xhr_header)
70 70
71 71 all_user_groups = UserGroup.query().count()
72 72 assert response.json['recordsTotal'] == all_user_groups
73 73 assert response.json['recordsFiltered'] == 0
74 74
75 75 def test_usergroup_escape(self, user_util, xhr_header):
76 76 self.log_user()
77 77
78 78 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
79 79 user = user_util.create_user()
80 80 user.name = xss_img
81 81 user.lastname = xss_img
82 82 Session().add(user)
83 83 Session().commit()
84 84
85 85 user_group = user_util.create_user_group()
86 86
87 87 user_group.users_group_name = xss_img
88 88 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
89 89
90 90 response = self.app.get(
91 91 route_path('user_groups_data'), extra_environ=xhr_header)
92 92
93 93 response.mustcontain(
94 94 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
95 95 response.mustcontain(
96 96 '&lt;img src=&#34;/image1&#34; onload=&#34;'
97 97 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
98 98
99 99 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
100 100 self.log_user()
101 101 ug = user_util.create_user_group()
102 102 response = self.app.get(
103 103 route_path('user_group_members_data', user_group_id=ug.users_group_id),
104 104 extra_environ=xhr_header)
105 105
106 106 assert response.json == {'members': []}
107 107
108 108 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
109 109 self.log_user()
110 110 members = [u.user_id for u in User.get_all()]
111 111 ug = user_util.create_user_group(members=members)
112 112 response = self.app.get(
113 113 route_path('user_group_members_data',
114 114 user_group_id=ug.users_group_id),
115 115 extra_environ=xhr_header)
116 116
117 117 assert len(response.json['members']) == len(members)
118 118
119 119 def test_creation_page(self):
120 120 self.log_user()
121 121 self.app.get(route_path('user_groups_new'), status=200)
122 122
123 123 def test_create(self):
124 124 from rhodecode.lib import helpers as h
125 125
126 126 self.log_user()
127 127 users_group_name = 'test_user_group'
128 128 response = self.app.post(route_path('user_groups_create'), {
129 129 'users_group_name': users_group_name,
130 130 'user_group_description': 'DESC',
131 131 'active': True,
132 132 'csrf_token': self.csrf_token})
133 133
134 134 user_group_id = UserGroup.get_by_group_name(
135 135 users_group_name).users_group_id
136 136
137 137 user_group_link = h.link_to(
138 138 users_group_name,
139 139 route_path('edit_user_group', user_group_id=user_group_id))
140 140
141 141 assert_session_flash(
142 142 response,
143 143 'Created user group %s' % user_group_link)
144 144
145 145 fixture.destroy_user_group(users_group_name)
146 146
147 147 def test_create_with_empty_name(self):
148 148 self.log_user()
149 149
150 150 response = self.app.post(route_path('user_groups_create'), {
151 151 'users_group_name': '',
152 152 'user_group_description': 'DESC',
153 153 'active': True,
154 154 'csrf_token': self.csrf_token}, status=200)
155 155
156 156 response.mustcontain('Please enter a value')
157 157
158 158 def test_create_duplicate(self, user_util):
159 159 self.log_user()
160 160
161 161 user_group = user_util.create_user_group()
162 162 duplicate_name = user_group.users_group_name
163 163 response = self.app.post(route_path('user_groups_create'), {
164 164 'users_group_name': duplicate_name,
165 165 'user_group_description': 'DESC',
166 166 'active': True,
167 167 'csrf_token': self.csrf_token}, status=200)
168 168
169 169 response.mustcontain(
170 170 'User group `{}` already exists'.format(duplicate_name))
@@ -1,794 +1,794 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22 from sqlalchemy.orm.exc import NoResultFound
23 23
24 24 from rhodecode.lib import auth
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.user import UserModel
29 29
30 30 from rhodecode.tests import (
31 31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
32 32 from rhodecode.tests.fixture import Fixture
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39 39 from rhodecode.apps._base import ADMIN_PREFIX
40 40
41 41 base_url = {
42 42 'users':
43 43 ADMIN_PREFIX + '/users',
44 44 'users_data':
45 45 ADMIN_PREFIX + '/users_data',
46 46 'users_create':
47 47 ADMIN_PREFIX + '/users/create',
48 48 'users_new':
49 49 ADMIN_PREFIX + '/users/new',
50 50 'user_edit':
51 51 ADMIN_PREFIX + '/users/{user_id}/edit',
52 52 'user_edit_advanced':
53 53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 54 'user_edit_global_perms':
55 55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 56 'user_edit_global_perms_update':
57 57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 58 'user_update':
59 59 ADMIN_PREFIX + '/users/{user_id}/update',
60 60 'user_delete':
61 61 ADMIN_PREFIX + '/users/{user_id}/delete',
62 62 'user_create_personal_repo_group':
63 63 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
64 64
65 65 'edit_user_auth_tokens':
66 66 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
67 67 'edit_user_auth_tokens_add':
68 68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
69 69 'edit_user_auth_tokens_delete':
70 70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
71 71
72 72 'edit_user_emails':
73 73 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
74 74 'edit_user_emails_add':
75 75 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
76 76 'edit_user_emails_delete':
77 77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
78 78
79 79 'edit_user_ips':
80 80 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
81 81 'edit_user_ips_add':
82 82 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
83 83 'edit_user_ips_delete':
84 84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
85 85
86 86 'edit_user_perms_summary':
87 87 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
88 88 'edit_user_perms_summary_json':
89 89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
90 90
91 91 'edit_user_audit_logs':
92 92 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
93 93
94 94 'edit_user_audit_logs_download':
95 95 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
96 96
97 97 }[name].format(**kwargs)
98 98
99 99 if params:
100 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
100 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
101 101 return base_url
102 102
103 103
104 104 class TestAdminUsersView(TestController):
105 105
106 106 def test_show_users(self):
107 107 self.log_user()
108 108 self.app.get(route_path('users'))
109 109
110 110 def test_show_users_data(self, xhr_header):
111 111 self.log_user()
112 112 response = self.app.get(route_path(
113 113 'users_data'), extra_environ=xhr_header)
114 114
115 115 all_users = User.query().filter(
116 116 User.username != User.DEFAULT_USER).count()
117 117 assert response.json['recordsTotal'] == all_users
118 118
119 119 def test_show_users_data_filtered(self, xhr_header):
120 120 self.log_user()
121 121 response = self.app.get(route_path(
122 122 'users_data', params={'search[value]': 'empty_search'}),
123 123 extra_environ=xhr_header)
124 124
125 125 all_users = User.query().filter(
126 126 User.username != User.DEFAULT_USER).count()
127 127 assert response.json['recordsTotal'] == all_users
128 128 assert response.json['recordsFiltered'] == 0
129 129
130 130 def test_auth_tokens_default_user(self):
131 131 self.log_user()
132 132 user = User.get_default_user()
133 133 response = self.app.get(
134 134 route_path('edit_user_auth_tokens', user_id=user.user_id),
135 135 status=302)
136 136
137 137 def test_auth_tokens(self):
138 138 self.log_user()
139 139
140 140 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
141 141 user_id = user.user_id
142 142 auth_tokens = user.auth_tokens
143 143 response = self.app.get(
144 144 route_path('edit_user_auth_tokens', user_id=user_id))
145 145 for token in auth_tokens:
146 146 response.mustcontain(token[:4])
147 147 response.mustcontain('never')
148 148
149 149 @pytest.mark.parametrize("desc, lifetime", [
150 150 ('forever', -1),
151 151 ('5mins', 60*5),
152 152 ('30days', 60*60*24*30),
153 153 ])
154 154 def test_add_auth_token(self, desc, lifetime, user_util):
155 155 self.log_user()
156 156 user = user_util.create_user()
157 157 user_id = user.user_id
158 158
159 159 response = self.app.post(
160 160 route_path('edit_user_auth_tokens_add', user_id=user_id),
161 161 {'description': desc, 'lifetime': lifetime,
162 162 'csrf_token': self.csrf_token})
163 163 assert_session_flash(response, 'Auth token successfully created')
164 164
165 165 response = response.follow()
166 166 user = User.get(user_id)
167 167 for auth_token in user.auth_tokens:
168 168 response.mustcontain(auth_token[:4])
169 169
170 170 def test_delete_auth_token(self, user_util):
171 171 self.log_user()
172 172 user = user_util.create_user()
173 173 user_id = user.user_id
174 174 keys = user.auth_tokens
175 175 assert 2 == len(keys)
176 176
177 177 response = self.app.post(
178 178 route_path('edit_user_auth_tokens_add', user_id=user_id),
179 179 {'description': 'desc', 'lifetime': -1,
180 180 'csrf_token': self.csrf_token})
181 181 assert_session_flash(response, 'Auth token successfully created')
182 182 response.follow()
183 183
184 184 # now delete our key
185 185 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
186 186 assert 3 == len(keys)
187 187
188 188 response = self.app.post(
189 189 route_path('edit_user_auth_tokens_delete', user_id=user_id),
190 190 {'del_auth_token': keys[0].user_api_key_id,
191 191 'csrf_token': self.csrf_token})
192 192
193 193 assert_session_flash(response, 'Auth token successfully deleted')
194 194 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
195 195 assert 2 == len(keys)
196 196
197 197 def test_ips(self):
198 198 self.log_user()
199 199 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
200 200 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
201 201 response.mustcontain('All IP addresses are allowed')
202 202
203 203 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
204 204 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
205 205 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
206 206 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
207 207 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
208 208 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
209 209 ('127_bad_ip', 'foobar', 'foobar', True),
210 210 ])
211 211 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
212 212 self.log_user()
213 213 user = user_util.create_user(username=test_name)
214 214 user_id = user.user_id
215 215
216 216 response = self.app.post(
217 217 route_path('edit_user_ips_add', user_id=user_id),
218 218 params={'new_ip': ip, 'csrf_token': self.csrf_token})
219 219
220 220 if failure:
221 221 assert_session_flash(
222 222 response, 'Please enter a valid IPv4 or IpV6 address')
223 223 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
224 224
225 225 response.mustcontain(no=[ip])
226 226 response.mustcontain(no=[ip_range])
227 227
228 228 else:
229 229 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
230 230 response.mustcontain(ip)
231 231 response.mustcontain(ip_range)
232 232
233 233 def test_ips_delete(self, user_util):
234 234 self.log_user()
235 235 user = user_util.create_user()
236 236 user_id = user.user_id
237 237 ip = '127.0.0.1/32'
238 238 ip_range = '127.0.0.1 - 127.0.0.1'
239 239 new_ip = UserModel().add_extra_ip(user_id, ip)
240 240 Session().commit()
241 241 new_ip_id = new_ip.ip_id
242 242
243 243 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
244 244 response.mustcontain(ip)
245 245 response.mustcontain(ip_range)
246 246
247 247 self.app.post(
248 248 route_path('edit_user_ips_delete', user_id=user_id),
249 249 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
250 250
251 251 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
252 252 response.mustcontain('All IP addresses are allowed')
253 253 response.mustcontain(no=[ip])
254 254 response.mustcontain(no=[ip_range])
255 255
256 256 def test_emails(self):
257 257 self.log_user()
258 258 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
259 259 response = self.app.get(
260 260 route_path('edit_user_emails', user_id=user.user_id))
261 261 response.mustcontain('No additional emails specified')
262 262
263 263 def test_emails_add(self, user_util):
264 264 self.log_user()
265 265 user = user_util.create_user()
266 266 user_id = user.user_id
267 267
268 268 self.app.post(
269 269 route_path('edit_user_emails_add', user_id=user_id),
270 270 params={'new_email': 'example@rhodecode.com',
271 271 'csrf_token': self.csrf_token})
272 272
273 273 response = self.app.get(
274 274 route_path('edit_user_emails', user_id=user_id))
275 275 response.mustcontain('example@rhodecode.com')
276 276
277 277 def test_emails_add_existing_email(self, user_util, user_regular):
278 278 existing_email = user_regular.email
279 279
280 280 self.log_user()
281 281 user = user_util.create_user()
282 282 user_id = user.user_id
283 283
284 284 response = self.app.post(
285 285 route_path('edit_user_emails_add', user_id=user_id),
286 286 params={'new_email': existing_email,
287 287 'csrf_token': self.csrf_token})
288 288 assert_session_flash(
289 289 response, 'This e-mail address is already taken')
290 290
291 291 response = self.app.get(
292 292 route_path('edit_user_emails', user_id=user_id))
293 293 response.mustcontain(no=[existing_email])
294 294
295 295 def test_emails_delete(self, user_util):
296 296 self.log_user()
297 297 user = user_util.create_user()
298 298 user_id = user.user_id
299 299
300 300 self.app.post(
301 301 route_path('edit_user_emails_add', user_id=user_id),
302 302 params={'new_email': 'example@rhodecode.com',
303 303 'csrf_token': self.csrf_token})
304 304
305 305 response = self.app.get(
306 306 route_path('edit_user_emails', user_id=user_id))
307 307 response.mustcontain('example@rhodecode.com')
308 308
309 309 user_email = UserEmailMap.query()\
310 310 .filter(UserEmailMap.email == 'example@rhodecode.com') \
311 311 .filter(UserEmailMap.user_id == user_id)\
312 312 .one()
313 313
314 314 del_email_id = user_email.email_id
315 315 self.app.post(
316 316 route_path('edit_user_emails_delete', user_id=user_id),
317 317 params={'del_email_id': del_email_id,
318 318 'csrf_token': self.csrf_token})
319 319
320 320 response = self.app.get(
321 321 route_path('edit_user_emails', user_id=user_id))
322 322 response.mustcontain(no=['example@rhodecode.com'])
323 323
324 324 def test_create(self, request, xhr_header):
325 325 self.log_user()
326 326 username = 'newtestuser'
327 327 password = 'test12'
328 328 password_confirmation = password
329 329 name = 'name'
330 330 lastname = 'lastname'
331 331 email = 'mail@mail.com'
332 332
333 333 self.app.get(route_path('users_new'))
334 334
335 335 response = self.app.post(route_path('users_create'), params={
336 336 'username': username,
337 337 'password': password,
338 338 'description': 'mr CTO',
339 339 'password_confirmation': password_confirmation,
340 340 'firstname': name,
341 341 'active': True,
342 342 'lastname': lastname,
343 343 'extern_name': 'rhodecode',
344 344 'extern_type': 'rhodecode',
345 345 'email': email,
346 346 'csrf_token': self.csrf_token,
347 347 })
348 348 user_link = h.link_to(
349 349 username,
350 350 route_path(
351 351 'user_edit', user_id=User.get_by_username(username).user_id))
352 352 assert_session_flash(response, 'Created user %s' % (user_link,))
353 353
354 354 @request.addfinalizer
355 355 def cleanup():
356 356 fixture.destroy_user(username)
357 357 Session().commit()
358 358
359 359 new_user = User.query().filter(User.username == username).one()
360 360
361 361 assert new_user.username == username
362 362 assert auth.check_password(password, new_user.password)
363 363 assert new_user.name == name
364 364 assert new_user.lastname == lastname
365 365 assert new_user.email == email
366 366
367 367 response = self.app.get(route_path('users_data'),
368 368 extra_environ=xhr_header)
369 369 response.mustcontain(username)
370 370
371 371 def test_create_err(self):
372 372 self.log_user()
373 373 username = 'new_user'
374 374 password = ''
375 375 name = 'name'
376 376 lastname = 'lastname'
377 377 email = 'errmail.com'
378 378
379 379 self.app.get(route_path('users_new'))
380 380
381 381 response = self.app.post(route_path('users_create'), params={
382 382 'username': username,
383 383 'password': password,
384 384 'name': name,
385 385 'active': False,
386 386 'lastname': lastname,
387 387 'description': 'mr CTO',
388 388 'email': email,
389 389 'csrf_token': self.csrf_token,
390 390 })
391 391
392 392 msg = u'Username "%(username)s" is forbidden'
393 393 msg = h.html_escape(msg % {'username': 'new_user'})
394 394 response.mustcontain('<span class="error-message">%s</span>' % msg)
395 395 response.mustcontain(
396 396 '<span class="error-message">Please enter a value</span>')
397 397 response.mustcontain(
398 398 '<span class="error-message">An email address must contain a'
399 399 ' single @</span>')
400 400
401 401 def get_user():
402 402 Session().query(User).filter(User.username == username).one()
403 403
404 404 with pytest.raises(NoResultFound):
405 405 get_user()
406 406
407 407 def test_new(self):
408 408 self.log_user()
409 409 self.app.get(route_path('users_new'))
410 410
411 411 @pytest.mark.parametrize("name, attrs", [
412 412 ('firstname', {'firstname': 'new_username'}),
413 413 ('lastname', {'lastname': 'new_username'}),
414 414 ('admin', {'admin': True}),
415 415 ('admin', {'admin': False}),
416 416 ('extern_type', {'extern_type': 'ldap'}),
417 417 ('extern_type', {'extern_type': None}),
418 418 ('extern_name', {'extern_name': 'test'}),
419 419 ('extern_name', {'extern_name': None}),
420 420 ('active', {'active': False}),
421 421 ('active', {'active': True}),
422 422 ('email', {'email': 'some@email.com'}),
423 423 ('language', {'language': 'de'}),
424 424 ('language', {'language': 'en'}),
425 425 ('description', {'description': 'hello CTO'}),
426 426 # ('new_password', {'new_password': 'foobar123',
427 427 # 'password_confirmation': 'foobar123'})
428 428 ])
429 429 def test_update(self, name, attrs, user_util):
430 430 self.log_user()
431 431 usr = user_util.create_user(
432 432 password='qweqwe',
433 433 email='testme@rhodecode.org',
434 434 extern_type='rhodecode',
435 435 extern_name='xxx',
436 436 )
437 437 user_id = usr.user_id
438 438 Session().commit()
439 439
440 440 params = usr.get_api_data()
441 441 cur_lang = params['language'] or 'en'
442 442 params.update({
443 443 'password_confirmation': '',
444 444 'new_password': '',
445 445 'language': cur_lang,
446 446 'csrf_token': self.csrf_token,
447 447 })
448 448 params.update({'new_password': ''})
449 449 params.update(attrs)
450 450 if name == 'email':
451 451 params['emails'] = [attrs['email']]
452 452 elif name == 'extern_type':
453 453 # cannot update this via form, expected value is original one
454 454 params['extern_type'] = "rhodecode"
455 455 elif name == 'extern_name':
456 456 # cannot update this via form, expected value is original one
457 457 params['extern_name'] = 'xxx'
458 458 # special case since this user is not
459 459 # logged in yet his data is not filled
460 460 # so we use creation data
461 461
462 462 response = self.app.post(
463 463 route_path('user_update', user_id=usr.user_id), params)
464 464 assert response.status_int == 302
465 465 assert_session_flash(response, 'User updated successfully')
466 466
467 467 updated_user = User.get(user_id)
468 468 updated_params = updated_user.get_api_data()
469 469 updated_params.update({'password_confirmation': ''})
470 470 updated_params.update({'new_password': ''})
471 471
472 472 del params['csrf_token']
473 473 assert params == updated_params
474 474
475 475 def test_update_and_migrate_password(
476 476 self, autologin_user, real_crypto_backend, user_util):
477 477
478 478 user = user_util.create_user()
479 479 temp_user = user.username
480 480 user.password = auth._RhodeCodeCryptoSha256().hash_create(
481 481 b'test123')
482 482 Session().add(user)
483 483 Session().commit()
484 484
485 485 params = user.get_api_data()
486 486
487 487 params.update({
488 488 'password_confirmation': 'qweqwe123',
489 489 'new_password': 'qweqwe123',
490 490 'language': 'en',
491 491 'csrf_token': autologin_user.csrf_token,
492 492 })
493 493
494 494 response = self.app.post(
495 495 route_path('user_update', user_id=user.user_id), params)
496 496 assert response.status_int == 302
497 497 assert_session_flash(response, 'User updated successfully')
498 498
499 499 # new password should be bcrypted, after log-in and transfer
500 500 user = User.get_by_username(temp_user)
501 501 assert user.password.startswith('$')
502 502
503 503 updated_user = User.get_by_username(temp_user)
504 504 updated_params = updated_user.get_api_data()
505 505 updated_params.update({'password_confirmation': 'qweqwe123'})
506 506 updated_params.update({'new_password': 'qweqwe123'})
507 507
508 508 del params['csrf_token']
509 509 assert params == updated_params
510 510
511 511 def test_delete(self):
512 512 self.log_user()
513 513 username = 'newtestuserdeleteme'
514 514
515 515 fixture.create_user(name=username)
516 516
517 517 new_user = Session().query(User)\
518 518 .filter(User.username == username).one()
519 519 response = self.app.post(
520 520 route_path('user_delete', user_id=new_user.user_id),
521 521 params={'csrf_token': self.csrf_token})
522 522
523 523 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
524 524
525 525 def test_delete_owner_of_repository(self, request, user_util):
526 526 self.log_user()
527 527 obj_name = 'test_repo'
528 528 usr = user_util.create_user()
529 529 username = usr.username
530 530 fixture.create_repo(obj_name, cur_user=usr.username)
531 531
532 532 new_user = Session().query(User)\
533 533 .filter(User.username == username).one()
534 534 response = self.app.post(
535 535 route_path('user_delete', user_id=new_user.user_id),
536 536 params={'csrf_token': self.csrf_token})
537 537
538 538 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
539 539 'Switch owners or remove those repositories:%s' % (username, obj_name)
540 540 assert_session_flash(response, msg)
541 541 fixture.destroy_repo(obj_name)
542 542
543 543 def test_delete_owner_of_repository_detaching(self, request, user_util):
544 544 self.log_user()
545 545 obj_name = 'test_repo'
546 546 usr = user_util.create_user(auto_cleanup=False)
547 547 username = usr.username
548 548 fixture.create_repo(obj_name, cur_user=usr.username)
549 549 Session().commit()
550 550
551 551 new_user = Session().query(User)\
552 552 .filter(User.username == username).one()
553 553 response = self.app.post(
554 554 route_path('user_delete', user_id=new_user.user_id),
555 555 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
556 556
557 557 msg = 'Detached 1 repositories'
558 558 assert_session_flash(response, msg)
559 559 fixture.destroy_repo(obj_name)
560 560
561 561 def test_delete_owner_of_repository_deleting(self, request, user_util):
562 562 self.log_user()
563 563 obj_name = 'test_repo'
564 564 usr = user_util.create_user(auto_cleanup=False)
565 565 username = usr.username
566 566 fixture.create_repo(obj_name, cur_user=usr.username)
567 567
568 568 new_user = Session().query(User)\
569 569 .filter(User.username == username).one()
570 570 response = self.app.post(
571 571 route_path('user_delete', user_id=new_user.user_id),
572 572 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
573 573
574 574 msg = 'Deleted 1 repositories'
575 575 assert_session_flash(response, msg)
576 576
577 577 def test_delete_owner_of_repository_group(self, request, user_util):
578 578 self.log_user()
579 579 obj_name = 'test_group'
580 580 usr = user_util.create_user()
581 581 username = usr.username
582 582 fixture.create_repo_group(obj_name, cur_user=usr.username)
583 583
584 584 new_user = Session().query(User)\
585 585 .filter(User.username == username).one()
586 586 response = self.app.post(
587 587 route_path('user_delete', user_id=new_user.user_id),
588 588 params={'csrf_token': self.csrf_token})
589 589
590 590 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
591 591 'Switch owners or remove those repository groups:%s' % (username, obj_name)
592 592 assert_session_flash(response, msg)
593 593 fixture.destroy_repo_group(obj_name)
594 594
595 595 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
596 596 self.log_user()
597 597 obj_name = 'test_group'
598 598 usr = user_util.create_user(auto_cleanup=False)
599 599 username = usr.username
600 600 fixture.create_repo_group(obj_name, cur_user=usr.username)
601 601
602 602 new_user = Session().query(User)\
603 603 .filter(User.username == username).one()
604 604 response = self.app.post(
605 605 route_path('user_delete', user_id=new_user.user_id),
606 606 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
607 607
608 608 msg = 'Deleted 1 repository groups'
609 609 assert_session_flash(response, msg)
610 610
611 611 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
612 612 self.log_user()
613 613 obj_name = 'test_group'
614 614 usr = user_util.create_user(auto_cleanup=False)
615 615 username = usr.username
616 616 fixture.create_repo_group(obj_name, cur_user=usr.username)
617 617
618 618 new_user = Session().query(User)\
619 619 .filter(User.username == username).one()
620 620 response = self.app.post(
621 621 route_path('user_delete', user_id=new_user.user_id),
622 622 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
623 623
624 624 msg = 'Detached 1 repository groups'
625 625 assert_session_flash(response, msg)
626 626 fixture.destroy_repo_group(obj_name)
627 627
628 628 def test_delete_owner_of_user_group(self, request, user_util):
629 629 self.log_user()
630 630 obj_name = 'test_user_group'
631 631 usr = user_util.create_user()
632 632 username = usr.username
633 633 fixture.create_user_group(obj_name, cur_user=usr.username)
634 634
635 635 new_user = Session().query(User)\
636 636 .filter(User.username == username).one()
637 637 response = self.app.post(
638 638 route_path('user_delete', user_id=new_user.user_id),
639 639 params={'csrf_token': self.csrf_token})
640 640
641 641 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
642 642 'Switch owners or remove those user groups:%s' % (username, obj_name)
643 643 assert_session_flash(response, msg)
644 644 fixture.destroy_user_group(obj_name)
645 645
646 646 def test_delete_owner_of_user_group_detaching(self, request, user_util):
647 647 self.log_user()
648 648 obj_name = 'test_user_group'
649 649 usr = user_util.create_user(auto_cleanup=False)
650 650 username = usr.username
651 651 fixture.create_user_group(obj_name, cur_user=usr.username)
652 652
653 653 new_user = Session().query(User)\
654 654 .filter(User.username == username).one()
655 655 try:
656 656 response = self.app.post(
657 657 route_path('user_delete', user_id=new_user.user_id),
658 658 params={'user_user_groups': 'detach',
659 659 'csrf_token': self.csrf_token})
660 660
661 661 msg = 'Detached 1 user groups'
662 662 assert_session_flash(response, msg)
663 663 finally:
664 664 fixture.destroy_user_group(obj_name)
665 665
666 666 def test_delete_owner_of_user_group_deleting(self, request, user_util):
667 667 self.log_user()
668 668 obj_name = 'test_user_group'
669 669 usr = user_util.create_user(auto_cleanup=False)
670 670 username = usr.username
671 671 fixture.create_user_group(obj_name, cur_user=usr.username)
672 672
673 673 new_user = Session().query(User)\
674 674 .filter(User.username == username).one()
675 675 response = self.app.post(
676 676 route_path('user_delete', user_id=new_user.user_id),
677 677 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
678 678
679 679 msg = 'Deleted 1 user groups'
680 680 assert_session_flash(response, msg)
681 681
682 682 def test_edit(self, user_util):
683 683 self.log_user()
684 684 user = user_util.create_user()
685 685 self.app.get(route_path('user_edit', user_id=user.user_id))
686 686
687 687 def test_edit_default_user_redirect(self):
688 688 self.log_user()
689 689 user = User.get_default_user()
690 690 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
691 691
692 692 @pytest.mark.parametrize(
693 693 'repo_create, repo_create_write, user_group_create, repo_group_create,'
694 694 'fork_create, inherit_default_permissions, expect_error,'
695 695 'expect_form_error', [
696 696 ('hg.create.none', 'hg.create.write_on_repogroup.false',
697 697 'hg.usergroup.create.false', 'hg.repogroup.create.false',
698 698 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
699 699 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
700 700 'hg.usergroup.create.false', 'hg.repogroup.create.false',
701 701 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
702 702 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
703 703 'hg.usergroup.create.true', 'hg.repogroup.create.true',
704 704 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
705 705 False),
706 706 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
707 707 'hg.usergroup.create.true', 'hg.repogroup.create.true',
708 708 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
709 709 True),
710 710 ('', '', '', '', '', '', True, False),
711 711 ])
712 712 def test_global_perms_on_user(
713 713 self, repo_create, repo_create_write, user_group_create,
714 714 repo_group_create, fork_create, expect_error, expect_form_error,
715 715 inherit_default_permissions, user_util):
716 716 self.log_user()
717 717 user = user_util.create_user()
718 718 uid = user.user_id
719 719
720 720 # ENABLE REPO CREATE ON A GROUP
721 721 perm_params = {
722 722 'inherit_default_permissions': False,
723 723 'default_repo_create': repo_create,
724 724 'default_repo_create_on_write': repo_create_write,
725 725 'default_user_group_create': user_group_create,
726 726 'default_repo_group_create': repo_group_create,
727 727 'default_fork_create': fork_create,
728 728 'default_inherit_default_permissions': inherit_default_permissions,
729 729 'csrf_token': self.csrf_token,
730 730 }
731 731 response = self.app.post(
732 732 route_path('user_edit_global_perms_update', user_id=uid),
733 733 params=perm_params)
734 734
735 735 if expect_form_error:
736 736 assert response.status_int == 200
737 737 response.mustcontain('Value must be one of')
738 738 else:
739 739 if expect_error:
740 740 msg = 'An error occurred during permissions saving'
741 741 else:
742 742 msg = 'User global permissions updated successfully'
743 743 ug = User.get(uid)
744 744 del perm_params['inherit_default_permissions']
745 745 del perm_params['csrf_token']
746 746 assert perm_params == ug.get_default_perms()
747 747 assert_session_flash(response, msg)
748 748
749 749 def test_global_permissions_initial_values(self, user_util):
750 750 self.log_user()
751 751 user = user_util.create_user()
752 752 uid = user.user_id
753 753 response = self.app.get(
754 754 route_path('user_edit_global_perms', user_id=uid))
755 755 default_user = User.get_default_user()
756 756 default_permissions = default_user.get_default_perms()
757 757 assert_response = response.assert_response()
758 758 expected_permissions = (
759 759 'default_repo_create', 'default_repo_create_on_write',
760 760 'default_fork_create', 'default_repo_group_create',
761 761 'default_user_group_create', 'default_inherit_default_permissions')
762 762 for permission in expected_permissions:
763 763 css_selector = '[name={}][checked=checked]'.format(permission)
764 764 element = assert_response.get_element(css_selector)
765 765 assert element.value == default_permissions[permission]
766 766
767 767 def test_perms_summary_page(self):
768 768 user = self.log_user()
769 769 response = self.app.get(
770 770 route_path('edit_user_perms_summary', user_id=user['user_id']))
771 771 for repo in Repository.query().all():
772 772 response.mustcontain(repo.repo_name)
773 773
774 774 def test_perms_summary_page_json(self):
775 775 user = self.log_user()
776 776 response = self.app.get(
777 777 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
778 778 for repo in Repository.query().all():
779 779 response.mustcontain(repo.repo_name)
780 780
781 781 def test_audit_log_page(self):
782 782 user = self.log_user()
783 783 self.app.get(
784 784 route_path('edit_user_audit_logs', user_id=user['user_id']))
785 785
786 786 def test_audit_log_page_download(self):
787 787 user = self.log_user()
788 788 user_id = user['user_id']
789 789 response = self.app.get(
790 790 route_path('edit_user_audit_logs_download', user_id=user_id))
791 791
792 792 assert response.content_disposition == \
793 793 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
794 794 assert response.content_type == "application/json"
@@ -1,176 +1,176 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.model.db import User, UserSshKeys
24 24
25 25 from rhodecode.tests import TestController, assert_session_flash
26 26 from rhodecode.tests.fixture import Fixture
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 33 from rhodecode.apps._base import ADMIN_PREFIX
34 34
35 35 base_url = {
36 36 'edit_user_ssh_keys':
37 37 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
38 38 'edit_user_ssh_keys_generate_keypair':
39 39 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
40 40 'edit_user_ssh_keys_add':
41 41 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
42 42 'edit_user_ssh_keys_delete':
43 43 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
44 44
45 45 }[name].format(**kwargs)
46 46
47 47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 49 return base_url
50 50
51 51
52 52 class TestAdminUsersSshKeysView(TestController):
53 53 INVALID_KEY = """\
54 54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 60 your_email@example.com
61 61 """
62 62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 69 'your_email@example.com'
70 70 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71 71
72 72 def test_ssh_keys_default_user(self):
73 73 self.log_user()
74 74 user = User.get_default_user()
75 75 self.app.get(
76 76 route_path('edit_user_ssh_keys', user_id=user.user_id),
77 77 status=302)
78 78
79 79 def test_add_ssh_key_error(self, user_util):
80 80 self.log_user()
81 81 user = user_util.create_user()
82 82 user_id = user.user_id
83 83
84 84 key_data = self.INVALID_KEY
85 85
86 86 desc = 'MY SSH KEY'
87 87 response = self.app.post(
88 88 route_path('edit_user_ssh_keys_add', user_id=user_id),
89 89 {'description': desc, 'key_data': key_data,
90 90 'csrf_token': self.csrf_token})
91 91 assert_session_flash(response, 'An error occurred during ssh '
92 92 'key saving: Unable to decode the key')
93 93
94 94 def test_ssh_key_duplicate(self, user_util):
95 95 self.log_user()
96 96 user = user_util.create_user()
97 97 user_id = user.user_id
98 98
99 99 key_data = self.VALID_KEY
100 100
101 101 desc = 'MY SSH KEY'
102 102 response = self.app.post(
103 103 route_path('edit_user_ssh_keys_add', user_id=user_id),
104 104 {'description': desc, 'key_data': key_data,
105 105 'csrf_token': self.csrf_token})
106 106 assert_session_flash(response, 'Ssh Key successfully created')
107 107 response.follow() # flush session flash
108 108
109 109 # add the same key AGAIN
110 110 desc = 'MY SSH KEY'
111 111 response = self.app.post(
112 112 route_path('edit_user_ssh_keys_add', user_id=user_id),
113 113 {'description': desc, 'key_data': key_data,
114 114 'csrf_token': self.csrf_token})
115 115
116 116 err = 'Such key with fingerprint `{}` already exists, ' \
117 117 'please use a different one'.format(self.FINGERPRINT)
118 118 assert_session_flash(response, 'An error occurred during ssh key '
119 119 'saving: {}'.format(err))
120 120
121 121 def test_add_ssh_key(self, user_util):
122 122 self.log_user()
123 123 user = user_util.create_user()
124 124 user_id = user.user_id
125 125
126 126 key_data = self.VALID_KEY
127 127
128 128 desc = 'MY SSH KEY'
129 129 response = self.app.post(
130 130 route_path('edit_user_ssh_keys_add', user_id=user_id),
131 131 {'description': desc, 'key_data': key_data,
132 132 'csrf_token': self.csrf_token})
133 133 assert_session_flash(response, 'Ssh Key successfully created')
134 134
135 135 response = response.follow()
136 136 response.mustcontain(desc)
137 137
138 138 def test_delete_ssh_key(self, user_util):
139 139 self.log_user()
140 140 user = user_util.create_user()
141 141 user_id = user.user_id
142 142
143 143 key_data = self.VALID_KEY
144 144
145 145 desc = 'MY SSH KEY'
146 146 response = self.app.post(
147 147 route_path('edit_user_ssh_keys_add', user_id=user_id),
148 148 {'description': desc, 'key_data': key_data,
149 149 'csrf_token': self.csrf_token})
150 150 assert_session_flash(response, 'Ssh Key successfully created')
151 151 response = response.follow() # flush the Session flash
152 152
153 153 # now delete our key
154 154 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
155 155 assert 1 == len(keys)
156 156
157 157 response = self.app.post(
158 158 route_path('edit_user_ssh_keys_delete', user_id=user_id),
159 159 {'del_ssh_key': keys[0].ssh_key_id,
160 160 'csrf_token': self.csrf_token})
161 161
162 162 assert_session_flash(response, 'Ssh key successfully deleted')
163 163 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
164 164 assert 0 == len(keys)
165 165
166 166 def test_generate_keypair(self, user_util):
167 167 self.log_user()
168 168 user = user_util.create_user()
169 169 user_id = user.user_id
170 170
171 171 response = self.app.get(
172 172 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
173 173
174 174 response.mustcontain('Private key')
175 175 response.mustcontain('Public key')
176 176 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,234 +1,234 b''
1 1 # -*- coding: utf-8 -*-
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 21 import logging
22 import urllib2
22 import urllib.request, urllib.error, urllib.parse
23 23 import os
24 24
25 25 import rhodecode
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps._base.navigation import navigation_list
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 30 from rhodecode.lib.utils2 import str2bool
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.model.update import UpdateModel
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class AdminSystemInfoSettingsView(BaseAppView):
38 38 def load_default_context(self):
39 39 c = self._get_local_tmpl_context()
40 40 return c
41 41
42 42 def get_env_data(self):
43 43 black_list = [
44 44 'NIX_LDFLAGS',
45 45 'NIX_CFLAGS_COMPILE',
46 46 'propagatedBuildInputs',
47 47 'propagatedNativeBuildInputs',
48 48 'postInstall',
49 49 'buildInputs',
50 50 'buildPhase',
51 51 'preShellHook',
52 52 'preShellHook',
53 53 'preCheck',
54 54 'preBuild',
55 55 'postShellHook',
56 56 'postFixup',
57 57 'postCheck',
58 58 'nativeBuildInputs',
59 59 'installPhase',
60 60 'installCheckPhase',
61 61 'checkPhase',
62 62 'configurePhase',
63 63 'shellHook'
64 64 ]
65 65 secret_list = [
66 66 'RHODECODE_USER_PASS'
67 67 ]
68 68
69 69 for k, v in sorted(os.environ.items()):
70 70 if k in black_list:
71 71 continue
72 72 if k in secret_list:
73 73 v = '*****'
74 74 yield k, v
75 75
76 76 @LoginRequired()
77 77 @HasPermissionAllDecorator('hg.admin')
78 78 def settings_system_info(self):
79 79 _ = self.request.translate
80 80 c = self.load_default_context()
81 81
82 82 c.active = 'system'
83 83 c.navlist = navigation_list(self.request)
84 84
85 85 # TODO(marcink), figure out how to allow only selected users to do this
86 86 c.allowed_to_snapshot = self._rhodecode_user.admin
87 87
88 88 snapshot = str2bool(self.request.params.get('snapshot'))
89 89
90 90 c.rhodecode_update_url = UpdateModel().get_update_url()
91 91 c.env_data = self.get_env_data()
92 92 server_info = system_info.get_system_info(self.request.environ)
93 93
94 94 for key, val in server_info.items():
95 95 setattr(c, key, val)
96 96
97 97 def val(name, subkey='human_value'):
98 98 return server_info[name][subkey]
99 99
100 100 def state(name):
101 101 return server_info[name]['state']
102 102
103 103 def val2(name):
104 104 val = server_info[name]['human_value']
105 105 state = server_info[name]['state']
106 106 return val, state
107 107
108 108 update_info_msg = _('Note: please make sure this server can '
109 109 'access `${url}` for the update link to work',
110 110 mapping=dict(url=c.rhodecode_update_url))
111 111 version = UpdateModel().get_stored_version()
112 112 is_outdated = UpdateModel().is_outdated(
113 113 rhodecode.__version__, version)
114 114 update_state = {
115 115 'type': 'warning',
116 116 'message': 'New version available: {}'.format(version)
117 117 } \
118 118 if is_outdated else {}
119 119 c.data_items = [
120 120 # update info
121 121 (_('Update info'), h.literal(
122 122 '<span class="link" id="check_for_update" >%s.</span>' % (
123 123 _('Check for updates')) +
124 124 '<br/> <span >%s.</span>' % (update_info_msg)
125 125 ), ''),
126 126
127 127 # RhodeCode specific
128 128 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
129 129 (_('Latest version'), version, update_state),
130 130 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
131 131 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
132 132 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
133 133 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
134 134 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
135 135 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
136 136 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
137 137 ('', '', ''), # spacer
138 138
139 139 # Database
140 140 (_('Database'), val('database')['url'], state('database')),
141 141 (_('Database version'), val('database')['version'], state('database')),
142 142 ('', '', ''), # spacer
143 143
144 144 # Platform/Python
145 145 (_('Platform'), val('platform')['name'], state('platform')),
146 146 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
147 147 (_('Lang'), val('locale'), state('locale')),
148 148 (_('Python version'), val('python')['version'], state('python')),
149 149 (_('Python path'), val('python')['executable'], state('python')),
150 150 ('', '', ''), # spacer
151 151
152 152 # Systems stats
153 153 (_('CPU'), val('cpu')['text'], state('cpu')),
154 154 (_('Load'), val('load')['text'], state('load')),
155 155 (_('Memory'), val('memory')['text'], state('memory')),
156 156 (_('Uptime'), val('uptime')['text'], state('uptime')),
157 157 ('', '', ''), # spacer
158 158
159 159 # ulimit
160 160 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
161 161
162 162 # Repo storage
163 163 (_('Storage location'), val('storage')['path'], state('storage')),
164 164 (_('Storage info'), val('storage')['text'], state('storage')),
165 165 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
166 166
167 167 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
168 168 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
169 169
170 170 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
171 171 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
172 172
173 173 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
174 174 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
175 175
176 176 (_('Search info'), val('search')['text'], state('search')),
177 177 (_('Search location'), val('search')['location'], state('search')),
178 178 ('', '', ''), # spacer
179 179
180 180 # VCS specific
181 181 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
182 182 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
183 183 (_('GIT'), val('git'), state('git')),
184 184 (_('HG'), val('hg'), state('hg')),
185 185 (_('SVN'), val('svn'), state('svn')),
186 186
187 187 ]
188 188
189 189 c.vcsserver_data_items = [
190 190 (k, v) for k,v in (val('vcs_server_config') or {}).items()
191 191 ]
192 192
193 193 if snapshot:
194 194 if c.allowed_to_snapshot:
195 195 c.data_items.pop(0) # remove server info
196 196 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
197 197 else:
198 198 h.flash('You are not allowed to do this', category='warning')
199 199 return self._get_template_context(c)
200 200
201 201 @LoginRequired()
202 202 @HasPermissionAllDecorator('hg.admin')
203 203 def settings_system_info_check_update(self):
204 204 _ = self.request.translate
205 205 c = self.load_default_context()
206 206
207 207 update_url = UpdateModel().get_update_url()
208 208
209 209 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
210 210 try:
211 211 data = UpdateModel().get_update_data(update_url)
212 except urllib2.URLError as e:
212 except urllib.error.URLError as e:
213 213 log.exception("Exception contacting upgrade server")
214 214 self.request.override_renderer = 'string'
215 215 return _err('Failed to contact upgrade server: %r' % e)
216 216 except ValueError as e:
217 217 log.exception("Bad data sent from update server")
218 218 self.request.override_renderer = 'string'
219 219 return _err('Bad data sent from update server')
220 220
221 221 latest = data['versions'][0]
222 222
223 223 c.update_url = update_url
224 224 c.latest_data = latest
225 225 c.latest_ver = latest['version']
226 226 c.cur_ver = rhodecode.__version__
227 227 c.should_upgrade = False
228 228
229 229 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
230 230 if is_oudated:
231 231 c.should_upgrade = True
232 232 c.important_notices = latest['general']
233 233 UpdateModel().store_version(latest['version'])
234 234 return self._get_template_context(c)
@@ -1,261 +1,261 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 import os
21 21 import pytest
22 22
23 23 from rhodecode.lib.ext_json import json
24 24 from rhodecode.model.auth_token import AuthTokenModel
25 25 from rhodecode.model.db import Session, FileStore, Repository, User
26 26 from rhodecode.tests import TestController
27 27 from rhodecode.apps.file_store import utils, config_keys
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32 32
33 33 base_url = {
34 34 'upload_file': '/_file_store/upload',
35 35 'download_file': '/_file_store/download/{fid}',
36 36 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
37 37
38 38 }[name].format(**kwargs)
39 39
40 40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 42 return base_url
43 43
44 44
45 45 class TestFileStoreViews(TestController):
46 46
47 47 @pytest.mark.parametrize("fid, content, exists", [
48 48 ('abcde-0.jpg', "xxxxx", True),
49 49 ('abcde-0.exe', "1234567", True),
50 50 ('abcde-0.jpg', "xxxxx", False),
51 51 ])
52 52 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 53 user = self.log_user()
54 54 user_id = user['user_id']
55 55 repo_id = user_util.create_repo().repo_id
56 56 store_path = self.app._pyramid_settings[config_keys.store_path]
57 57 store_uid = fid
58 58
59 59 if exists:
60 60 status = 200
61 61 store = utils.get_file_storage({config_keys.store_path: store_path})
62 62 filesystem_file = os.path.join(str(tmpdir), fid)
63 63 with open(filesystem_file, 'wb') as f:
64 64 f.write(content)
65 65
66 66 with open(filesystem_file, 'rb') as f:
67 67 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68 68
69 69 entry = FileStore.create(
70 70 file_uid=store_uid, filename=metadata["filename"],
71 71 file_hash=metadata["sha256"], file_size=metadata["size"],
72 72 file_display_name='file_display_name',
73 73 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 74 check_acl=True, user_id=user_id,
75 75 scope_repo_id=repo_id
76 76 )
77 77 Session().add(entry)
78 78 Session().commit()
79 79
80 80 else:
81 81 status = 404
82 82
83 83 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84 84
85 85 if exists:
86 86 assert response.text == content
87 87 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 88 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 89 assert os.path.exists(metadata_file)
90 90 with open(metadata_file, 'rb') as f:
91 91 json_data = json.loads(f.read())
92 92
93 93 assert json_data
94 94 assert 'size' in json_data
95 95
96 96 def test_upload_files_without_content_to_store(self):
97 97 self.log_user()
98 98 response = self.app.post(
99 99 route_path('upload_file'),
100 100 params={'csrf_token': self.csrf_token},
101 101 status=200)
102 102
103 103 assert response.json == {
104 104 u'error': u'store_file data field is missing',
105 105 u'access_path': None,
106 106 u'store_fid': None}
107 107
108 108 def test_upload_files_bogus_content_to_store(self):
109 109 self.log_user()
110 110 response = self.app.post(
111 111 route_path('upload_file'),
112 112 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 113 status=200)
114 114
115 115 assert response.json == {
116 116 u'error': u'filename cannot be read from the data field',
117 117 u'access_path': None,
118 118 u'store_fid': None}
119 119
120 120 def test_upload_content_to_store(self):
121 121 self.log_user()
122 122 response = self.app.post(
123 123 route_path('upload_file'),
124 124 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
125 125 params={'csrf_token': self.csrf_token},
126 126 status=200)
127 127
128 128 assert response.json['store_fid']
129 129
130 130 @pytest.fixture()
131 131 def create_artifact_factory(self, tmpdir):
132 132 def factory(user_id, content):
133 133 store_path = self.app._pyramid_settings[config_keys.store_path]
134 134 store = utils.get_file_storage({config_keys.store_path: store_path})
135 135 fid = 'example.txt'
136 136
137 137 filesystem_file = os.path.join(str(tmpdir), fid)
138 138 with open(filesystem_file, 'wb') as f:
139 139 f.write(content)
140 140
141 141 with open(filesystem_file, 'rb') as f:
142 142 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143 143
144 144 entry = FileStore.create(
145 145 file_uid=store_uid, filename=metadata["filename"],
146 146 file_hash=metadata["sha256"], file_size=metadata["size"],
147 147 file_display_name='file_display_name',
148 148 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 149 check_acl=True, user_id=user_id,
150 150 )
151 151 Session().add(entry)
152 152 Session().commit()
153 153 return entry
154 154 return factory
155 155
156 156 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 157 user = self.log_user()
158 158 user_id = user['user_id']
159 159 content = 'HELLO MY NAME IS ARTIFACT !'
160 160
161 161 artifact = create_artifact_factory(user_id, content)
162 162 file_uid = artifact.file_uid
163 163 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 164 assert response.text == content
165 165
166 166 # log-in to new user and test download again
167 167 user = user_util.create_user(password='qweqwe')
168 168 self.log_user(user.username, 'qweqwe')
169 169 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 170 assert response.text == content
171 171
172 172 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 173 user = self.log_user()
174 174 user_id = user['user_id']
175 175 content = 'HELLO MY NAME IS ARTIFACT !'
176 176
177 177 artifact = create_artifact_factory(user_id, content)
178 178 # bind to repo
179 179 repo = user_util.create_repo()
180 180 repo_id = repo.repo_id
181 181 artifact.scope_repo_id = repo_id
182 182 Session().add(artifact)
183 183 Session().commit()
184 184
185 185 file_uid = artifact.file_uid
186 186 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 187 assert response.text == content
188 188
189 189 # log-in to new user and test download again
190 190 user = user_util.create_user(password='qweqwe')
191 191 self.log_user(user.username, 'qweqwe')
192 192 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 193 assert response.text == content
194 194
195 195 # forbid user the rights to repo
196 196 repo = Repository.get(repo_id)
197 197 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 198 self.app.get(route_path('download_file', fid=file_uid), status=404)
199 199
200 200 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 201 user = self.log_user()
202 202 user_id = user['user_id']
203 203 content = 'HELLO MY NAME IS ARTIFACT !'
204 204
205 205 artifact = create_artifact_factory(user_id, content)
206 206 # bind to user
207 207 user = user_util.create_user(password='qweqwe')
208 208
209 209 artifact.scope_user_id = user.user_id
210 210 Session().add(artifact)
211 211 Session().commit()
212 212
213 213 # artifact creator doesn't have access since it's bind to another user
214 214 file_uid = artifact.file_uid
215 215 self.app.get(route_path('download_file', fid=file_uid), status=404)
216 216
217 217 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 218 self.log_user(user.username, 'qweqwe')
219 219 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 220 assert response.text == content
221 221
222 222 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 223 user_id = User.get_first_super_admin().user_id
224 224 content = 'HELLO MY NAME IS ARTIFACT !'
225 225
226 226 artifact = create_artifact_factory(user_id, content)
227 227 # bind to repo
228 228 repo = user_util.create_repo()
229 229 repo_id = repo.repo_id
230 230 artifact.scope_repo_id = repo_id
231 231 Session().add(artifact)
232 232 Session().commit()
233 233
234 234 file_uid = artifact.file_uid
235 235 self.app.get(route_path('download_file_by_token',
236 236 _auth_token='bogus', fid=file_uid), status=302)
237 237
238 238 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 239 user = User.get_first_super_admin()
240 240 AuthTokenModel().create(user, 'test artifact token',
241 241 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242 242
243 243 user = User.get_first_super_admin()
244 244 artifact_token = user.artifact_token
245 245
246 246 user_id = User.get_first_super_admin().user_id
247 247 content = 'HELLO MY NAME IS ARTIFACT !'
248 248
249 249 artifact = create_artifact_factory(user_id, content)
250 250 # bind to repo
251 251 repo = user_util.create_repo()
252 252 repo_id = repo.repo_id
253 253 artifact.scope_repo_id = repo_id
254 254 Session().add(artifact)
255 255 Session().commit()
256 256
257 257 file_uid = artifact.file_uid
258 258 response = self.app.get(
259 259 route_path('download_file_by_token',
260 260 _auth_token=artifact_token, fid=file_uid), status=200)
261 261 assert response.text == content
@@ -1,391 +1,391 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.model.db import User, Gist
26 26 from rhodecode.model.gist import GistModel
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.tests import (
29 29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 30 TestController, assert_session_flash)
31 31
32 32
33 33 def route_path(name, params=None, **kwargs):
34 import urllib
34 import urllib.request, urllib.parse, urllib.error
35 35 from rhodecode.apps._base import ADMIN_PREFIX
36 36
37 37 base_url = {
38 38 'gists_show': ADMIN_PREFIX + '/gists',
39 39 'gists_new': ADMIN_PREFIX + '/gists/new',
40 40 'gists_create': ADMIN_PREFIX + '/gists/create',
41 41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
42 42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
43 43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
44 44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
45 45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
46 46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
47 47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
48 48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
49 49
50 50 }[name].format(**kwargs)
51 51
52 52 if params:
53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 54 return base_url
55 55
56 56
57 57 class GistUtility(object):
58 58
59 59 def __init__(self):
60 60 self._gist_ids = []
61 61
62 62 def __call__(
63 63 self, f_name, content='some gist', lifetime=-1,
64 64 description='gist-desc', gist_type='public',
65 65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
66 66 gist_mapping = {
67 67 f_name: {'content': content}
68 68 }
69 69 user = User.get_by_username(owner)
70 70 gist = GistModel().create(
71 71 description, owner=user, gist_mapping=gist_mapping,
72 72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
73 73 Session().commit()
74 74 self._gist_ids.append(gist.gist_id)
75 75 return gist
76 76
77 77 def cleanup(self):
78 78 for gist_id in self._gist_ids:
79 79 gist = Gist.get(gist_id)
80 80 if gist:
81 81 Session().delete(gist)
82 82
83 83 Session().commit()
84 84
85 85
86 86 @pytest.fixture()
87 87 def create_gist(request):
88 88 gist_utility = GistUtility()
89 89 request.addfinalizer(gist_utility.cleanup)
90 90 return gist_utility
91 91
92 92
93 93 class TestGistsController(TestController):
94 94
95 95 def test_index_empty(self, create_gist):
96 96 self.log_user()
97 97 response = self.app.get(route_path('gists_show'))
98 98 response.mustcontain('data: [],')
99 99
100 100 def test_index(self, create_gist):
101 101 self.log_user()
102 102 g1 = create_gist('gist1')
103 103 g2 = create_gist('gist2', lifetime=1400)
104 104 g3 = create_gist('gist3', description='gist3-desc')
105 105 g4 = create_gist('gist4', gist_type='private').gist_access_id
106 106 response = self.app.get(route_path('gists_show'))
107 107
108 108 response.mustcontain(g1.gist_access_id)
109 109 response.mustcontain(g2.gist_access_id)
110 110 response.mustcontain(g3.gist_access_id)
111 111 response.mustcontain('gist3-desc')
112 112 response.mustcontain(no=[g4])
113 113
114 114 # Expiration information should be visible
115 115 expires_tag = '%s' % h.age_component(
116 116 h.time_to_utcdatetime(g2.gist_expires))
117 117 response.mustcontain(expires_tag.replace('"', '\\"'))
118 118
119 119 def test_index_private_gists(self, create_gist):
120 120 self.log_user()
121 121 gist = create_gist('gist5', gist_type='private')
122 122 response = self.app.get(route_path('gists_show', params=dict(private=1)))
123 123
124 124 # and privates
125 125 response.mustcontain(gist.gist_access_id)
126 126
127 127 def test_index_show_all(self, create_gist):
128 128 self.log_user()
129 129 create_gist('gist1')
130 130 create_gist('gist2', lifetime=1400)
131 131 create_gist('gist3', description='gist3-desc')
132 132 create_gist('gist4', gist_type='private')
133 133
134 134 response = self.app.get(route_path('gists_show', params=dict(all=1)))
135 135
136 136 assert len(GistModel.get_all()) == 4
137 137 # and privates
138 138 for gist in GistModel.get_all():
139 139 response.mustcontain(gist.gist_access_id)
140 140
141 141 def test_index_show_all_hidden_from_regular(self, create_gist):
142 142 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
143 143 create_gist('gist2', gist_type='private')
144 144 create_gist('gist3', gist_type='private')
145 145 create_gist('gist4', gist_type='private')
146 146
147 147 response = self.app.get(route_path('gists_show', params=dict(all=1)))
148 148
149 149 assert len(GistModel.get_all()) == 3
150 150 # since we don't have access to private in this view, we
151 151 # should see nothing
152 152 for gist in GistModel.get_all():
153 153 response.mustcontain(no=[gist.gist_access_id])
154 154
155 155 def test_create(self):
156 156 self.log_user()
157 157 response = self.app.post(
158 158 route_path('gists_create'),
159 159 params={'lifetime': -1,
160 160 'content': 'gist test',
161 161 'filename': 'foo',
162 162 'gist_type': 'public',
163 163 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
164 164 'csrf_token': self.csrf_token},
165 165 status=302)
166 166 response = response.follow()
167 167 response.mustcontain('added file: foo')
168 168 response.mustcontain('gist test')
169 169
170 170 def test_create_with_path_with_dirs(self):
171 171 self.log_user()
172 172 response = self.app.post(
173 173 route_path('gists_create'),
174 174 params={'lifetime': -1,
175 175 'content': 'gist test',
176 176 'filename': '/home/foo',
177 177 'gist_type': 'public',
178 178 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
179 179 'csrf_token': self.csrf_token},
180 180 status=200)
181 181 response.mustcontain('Filename /home/foo cannot be inside a directory')
182 182
183 183 def test_access_expired_gist(self, create_gist):
184 184 self.log_user()
185 185 gist = create_gist('never-see-me')
186 186 gist.gist_expires = 0 # 1970
187 187 Session().add(gist)
188 188 Session().commit()
189 189
190 190 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
191 191 status=404)
192 192
193 193 def test_create_private(self):
194 194 self.log_user()
195 195 response = self.app.post(
196 196 route_path('gists_create'),
197 197 params={'lifetime': -1,
198 198 'content': 'private gist test',
199 199 'filename': 'private-foo',
200 200 'gist_type': 'private',
201 201 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
202 202 'csrf_token': self.csrf_token},
203 203 status=302)
204 204 response = response.follow()
205 205 response.mustcontain('added file: private-foo<')
206 206 response.mustcontain('private gist test')
207 207 response.mustcontain('Private Gist')
208 208 # Make sure private gists are not indexed by robots
209 209 response.mustcontain(
210 210 '<meta name="robots" content="noindex, nofollow">')
211 211
212 212 def test_create_private_acl_private(self):
213 213 self.log_user()
214 214 response = self.app.post(
215 215 route_path('gists_create'),
216 216 params={'lifetime': -1,
217 217 'content': 'private gist test',
218 218 'filename': 'private-foo',
219 219 'gist_type': 'private',
220 220 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
221 221 'csrf_token': self.csrf_token},
222 222 status=302)
223 223 response = response.follow()
224 224 response.mustcontain('added file: private-foo<')
225 225 response.mustcontain('private gist test')
226 226 response.mustcontain('Private Gist')
227 227 # Make sure private gists are not indexed by robots
228 228 response.mustcontain(
229 229 '<meta name="robots" content="noindex, nofollow">')
230 230
231 231 def test_create_with_description(self):
232 232 self.log_user()
233 233 response = self.app.post(
234 234 route_path('gists_create'),
235 235 params={'lifetime': -1,
236 236 'content': 'gist test',
237 237 'filename': 'foo-desc',
238 238 'description': 'gist-desc',
239 239 'gist_type': 'public',
240 240 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
241 241 'csrf_token': self.csrf_token},
242 242 status=302)
243 243 response = response.follow()
244 244 response.mustcontain('added file: foo-desc')
245 245 response.mustcontain('gist test')
246 246 response.mustcontain('gist-desc')
247 247
248 248 def test_create_public_with_anonymous_access(self):
249 249 self.log_user()
250 250 params = {
251 251 'lifetime': -1,
252 252 'content': 'gist test',
253 253 'filename': 'foo-desc',
254 254 'description': 'gist-desc',
255 255 'gist_type': 'public',
256 256 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
257 257 'csrf_token': self.csrf_token
258 258 }
259 259 response = self.app.post(
260 260 route_path('gists_create'), params=params, status=302)
261 261 self.logout_user()
262 262 response = response.follow()
263 263 response.mustcontain('added file: foo-desc')
264 264 response.mustcontain('gist test')
265 265 response.mustcontain('gist-desc')
266 266
267 267 def test_new(self):
268 268 self.log_user()
269 269 self.app.get(route_path('gists_new'))
270 270
271 271 def test_delete(self, create_gist):
272 272 self.log_user()
273 273 gist = create_gist('delete-me')
274 274 response = self.app.post(
275 275 route_path('gist_delete', gist_id=gist.gist_id),
276 276 params={'csrf_token': self.csrf_token})
277 277 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
278 278
279 279 def test_delete_normal_user_his_gist(self, create_gist):
280 280 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
281 281 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
282 282
283 283 response = self.app.post(
284 284 route_path('gist_delete', gist_id=gist.gist_id),
285 285 params={'csrf_token': self.csrf_token})
286 286 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
287 287
288 288 def test_delete_normal_user_not_his_own_gist(self, create_gist):
289 289 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
290 290 gist = create_gist('delete-me-2')
291 291
292 292 self.app.post(
293 293 route_path('gist_delete', gist_id=gist.gist_id),
294 294 params={'csrf_token': self.csrf_token}, status=404)
295 295
296 296 def test_show(self, create_gist):
297 297 gist = create_gist('gist-show-me')
298 298 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
299 299
300 300 response.mustcontain('added file: gist-show-me<')
301 301
302 302 assert_response = response.assert_response()
303 303 assert_response.element_equals_to(
304 304 'div.rc-user span.user',
305 305 '<a href="/_profiles/test_admin">test_admin</a>')
306 306
307 307 response.mustcontain('gist-desc')
308 308
309 309 def test_show_without_hg(self, create_gist):
310 310 with mock.patch(
311 311 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
312 312 gist = create_gist('gist-show-me-again')
313 313 self.app.get(
314 314 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
315 315
316 316 def test_show_acl_private(self, create_gist):
317 317 gist = create_gist('gist-show-me-only-when-im-logged-in',
318 318 acl_level=Gist.ACL_LEVEL_PRIVATE)
319 319 self.app.get(
320 320 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
321 321
322 322 # now we log-in we should see thi gist
323 323 self.log_user()
324 324 response = self.app.get(
325 325 route_path('gist_show', gist_id=gist.gist_access_id))
326 326 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
327 327
328 328 assert_response = response.assert_response()
329 329 assert_response.element_equals_to(
330 330 'div.rc-user span.user',
331 331 '<a href="/_profiles/test_admin">test_admin</a>')
332 332 response.mustcontain('gist-desc')
333 333
334 334 def test_show_as_raw(self, create_gist):
335 335 gist = create_gist('gist-show-me', content='GIST CONTENT')
336 336 response = self.app.get(
337 337 route_path('gist_show_formatted',
338 338 gist_id=gist.gist_access_id, revision='tip',
339 339 format='raw'))
340 340 assert response.body == 'GIST CONTENT'
341 341
342 342 def test_show_as_raw_individual_file(self, create_gist):
343 343 gist = create_gist('gist-show-me-raw', content='GIST BODY')
344 344 response = self.app.get(
345 345 route_path('gist_show_formatted_path',
346 346 gist_id=gist.gist_access_id, format='raw',
347 347 revision='tip', f_path='gist-show-me-raw'))
348 348 assert response.body == 'GIST BODY'
349 349
350 350 def test_edit_page(self, create_gist):
351 351 self.log_user()
352 352 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
353 353 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
354 354 response.mustcontain('GIST EDIT BODY')
355 355
356 356 def test_edit_page_non_logged_user(self, create_gist):
357 357 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
358 358 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
359 359 status=302)
360 360
361 361 def test_edit_normal_user_his_gist(self, create_gist):
362 362 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
363 363 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
364 364 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
365 365 status=200))
366 366
367 367 def test_edit_normal_user_not_his_own_gist(self, create_gist):
368 368 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
369 369 gist = create_gist('delete-me')
370 370 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
371 371 status=404)
372 372
373 373 def test_user_first_name_is_escaped(self, user_util, create_gist):
374 374 xss_atack_string = '"><script>alert(\'First Name\')</script>'
375 375 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
376 376 password = 'test'
377 377 user = user_util.create_user(
378 378 firstname=xss_atack_string, password=password)
379 379 create_gist('gist', gist_type='public', owner=user.username)
380 380 response = self.app.get(route_path('gists_show'))
381 381 response.mustcontain(xss_escaped_string)
382 382
383 383 def test_user_last_name_is_escaped(self, user_util, create_gist):
384 384 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
385 385 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
386 386 password = 'test'
387 387 user = user_util.create_user(
388 388 lastname=xss_atack_string, password=password)
389 389 create_gist('gist', gist_type='public', owner=user.username)
390 390 response = self.app.get(route_path('gists_show'))
391 391 response.mustcontain(xss_escaped_string)
@@ -1,180 +1,180 b''
1 1 # -*- coding: utf-8 -*-
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 21 import json
22 22
23 23 import pytest
24 24
25 25 from . import assert_and_get_main_filter_content
26 26 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 from rhodecode.lib.utils import map_groups
30 30 from rhodecode.model.repo import RepoModel
31 31 from rhodecode.model.repo_group import RepoGroupModel
32 32 from rhodecode.model.db import Session, Repository, RepoGroup
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39 39
40 40 base_url = {
41 41 'goto_switcher_data': '/_goto_data',
42 42 }[name].format(**kwargs)
43 43
44 44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 46 return base_url
47 47
48 48
49 49 class TestGotoSwitcherData(TestController):
50 50
51 51 required_repos_with_groups = [
52 52 'abc',
53 53 'abc-fork',
54 54 'forks/abcd',
55 55 'abcd',
56 56 'abcde',
57 57 'a/abc',
58 58 'aa/abc',
59 59 'aaa/abc',
60 60 'aaaa/abc',
61 61 'repos_abc/aaa/abc',
62 62 'abc_repos/abc',
63 63 'abc_repos/abcd',
64 64 'xxx/xyz',
65 65 'forked-abc/a/abc'
66 66 ]
67 67
68 68 @pytest.fixture(autouse=True, scope='class')
69 69 def prepare(self, request, baseapp):
70 70 for repo_and_group in self.required_repos_with_groups:
71 71 # create structure of groups and return the last group
72 72
73 73 repo_group = map_groups(repo_and_group)
74 74
75 75 RepoModel()._create_repo(
76 76 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
77 77 repo_group=getattr(repo_group, 'group_id', None))
78 78
79 79 Session().commit()
80 80
81 81 request.addfinalizer(self.cleanup)
82 82
83 83 def cleanup(self):
84 84 # first delete all repos
85 85 for repo_and_groups in self.required_repos_with_groups:
86 86 repo = Repository.get_by_repo_name(repo_and_groups)
87 87 if repo:
88 88 RepoModel().delete(repo)
89 89 Session().commit()
90 90
91 91 # then delete all empty groups
92 92 for repo_and_groups in self.required_repos_with_groups:
93 93 if '/' in repo_and_groups:
94 94 r_group = repo_and_groups.rsplit('/', 1)[0]
95 95 repo_group = RepoGroup.get_by_group_name(r_group)
96 96 if not repo_group:
97 97 continue
98 98 parents = repo_group.parents
99 99 RepoGroupModel().delete(repo_group, force_delete=True)
100 100 Session().commit()
101 101
102 102 for el in reversed(parents):
103 103 RepoGroupModel().delete(el, force_delete=True)
104 104 Session().commit()
105 105
106 106 def test_empty_query(self, xhr_header):
107 107 self.log_user()
108 108
109 109 response = self.app.get(
110 110 route_path('goto_switcher_data'),
111 111 extra_environ=xhr_header, status=200)
112 112 result = json.loads(response.body)['suggestions']
113 113
114 114 assert result == []
115 115
116 116 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
117 117 self.log_user()
118 118
119 119 response = self.app.get(
120 120 route_path('goto_switcher_data'),
121 121 params={'query': 'abc'},
122 122 extra_environ=xhr_header, status=200)
123 123 result = json.loads(response.body)['suggestions']
124 124
125 125 repos, groups, users, commits = assert_and_get_main_filter_content(result)
126 126
127 127 assert len(repos) == 13
128 128 assert len(groups) == 5
129 129 assert len(users) == 0
130 130 assert len(commits) == 0
131 131
132 132 def test_returns_list_of_users_filtered(self, xhr_header):
133 133 self.log_user()
134 134
135 135 response = self.app.get(
136 136 route_path('goto_switcher_data'),
137 137 params={'query': 'user:admin'},
138 138 extra_environ=xhr_header, status=200)
139 139 result = json.loads(response.body)['suggestions']
140 140
141 141 repos, groups, users, commits = assert_and_get_main_filter_content(result)
142 142
143 143 assert len(repos) == 0
144 144 assert len(groups) == 0
145 145 assert len(users) == 1
146 146 assert len(commits) == 0
147 147
148 148 def test_returns_list_of_commits_filtered(self, xhr_header):
149 149 self.log_user()
150 150
151 151 response = self.app.get(
152 152 route_path('goto_switcher_data'),
153 153 params={'query': 'commit:e8'},
154 154 extra_environ=xhr_header, status=200)
155 155 result = json.loads(response.body)['suggestions']
156 156
157 157 repos, groups, users, commits = assert_and_get_main_filter_content(result)
158 158
159 159 assert len(repos) == 0
160 160 assert len(groups) == 0
161 161 assert len(users) == 0
162 162 assert len(commits) == 5
163 163
164 164 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
165 165 self.log_user()
166 166
167 167 response = self.app.get(
168 168 route_path('goto_switcher_data'),
169 169 params={'query': 'abc'},
170 170 extra_environ=xhr_header, status=200)
171 171 result = json.loads(response.body)['suggestions']
172 172
173 173 repos, groups, users, commits = assert_and_get_main_filter_content(result)
174 174
175 175 test_repos = [x['value_display'] for x in repos[:4]]
176 176 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
177 177
178 178 test_groups = [x['value_display'] for x in groups[:4]]
179 179 assert ['abc_repos', 'repos_abc',
180 180 'forked-abc', 'forked-abc/a'] == test_groups
@@ -1,95 +1,95 b''
1 1 # -*- coding: utf-8 -*-
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 21 import json
22 22
23 23 from . import assert_and_get_repo_list_content
24 24 from rhodecode.tests import TestController
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.model.db import Repository
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 33
34 34 base_url = {
35 35 'repo_list_data': '/_repos',
36 36 }[name].format(**kwargs)
37 37
38 38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 40 return base_url
41 41
42 42
43 43 class TestRepoListData(TestController):
44 44
45 45 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 46 self.log_user()
47 47
48 48 response = self.app.get(
49 49 route_path('repo_list_data'),
50 50 extra_environ=xhr_header, status=200)
51 51 result = json.loads(response.body)['results']
52 52
53 53 repos = assert_and_get_repo_list_content(result)
54 54
55 55 assert len(repos) == len(Repository.get_all())
56 56
57 57 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 58 self.log_user()
59 59
60 60 response = self.app.get(
61 61 route_path('repo_list_data'),
62 62 params={'query': 'vcs_test_git'},
63 63 extra_environ=xhr_header, status=200)
64 64 result = json.loads(response.body)['results']
65 65
66 66 repos = assert_and_get_repo_list_content(result)
67 67
68 68 assert len(repos) == len(Repository.query().filter(
69 69 Repository.repo_name.ilike('%vcs_test_git%')).all())
70 70
71 71 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 72 self.log_user()
73 73
74 74 response = self.app.get(
75 75 route_path('repo_list_data'),
76 76 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 77 extra_environ=xhr_header, status=200)
78 78 result = json.loads(response.body)['results']
79 79
80 80 repos = assert_and_get_repo_list_content(result)
81 81
82 82 assert len(repos) == len(Repository.query().filter(
83 83 Repository.repo_name.ilike('%vcs_test_git%')).all())
84 84
85 85 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 86 self.log_user()
87 87 response = self.app.get(
88 88 route_path('repo_list_data'),
89 89 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 90 extra_environ=xhr_header, status=200)
91 91 result = json.loads(response.body)['results']
92 92
93 93 repos = assert_and_get_repo_list_content(result)
94 94
95 95 assert len(repos) == 0
@@ -1,112 +1,112 b''
1 1 # -*- coding: utf-8 -*-
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 21 import json
22 22 import pytest
23 23
24 24 from rhodecode.tests import TestController
25 25 from rhodecode.tests.fixture import Fixture
26 26
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, 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 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
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,117 +1,117 b''
1 1 # -*- coding: utf-8 -*-
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 # -*- coding: utf-8 -*-
21 21
22 22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 23 #
24 24 # This program is free software: you can redistribute it and/or modify
25 25 # it under the terms of the GNU Affero General Public License, version 3
26 26 # (only), as published by the Free Software Foundation.
27 27 #
28 28 # This program is distributed in the hope that it will be useful,
29 29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 31 # GNU General Public License for more details.
32 32 #
33 33 # You should have received a copy of the GNU Affero General Public License
34 34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 35 #
36 36 # This program is dual-licensed. If you wish to learn more about the
37 37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39 39
40 40 import json
41 41
42 42 import pytest
43 43
44 44 from rhodecode.tests import TestController
45 45 from rhodecode.tests.fixture import Fixture
46 46
47 47
48 48 fixture = Fixture()
49 49
50 50
51 51 def route_path(name, params=None, **kwargs):
52 import urllib
52 import urllib.request, urllib.parse, urllib.error
53 53
54 54 base_url = {
55 55 'user_autocomplete_data': '/_users',
56 56 'user_group_autocomplete_data': '/_user_groups'
57 57 }[name].format(**kwargs)
58 58
59 59 if params:
60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
61 61 return base_url
62 62
63 63
64 64 class TestUserGroupAutocompleteData(TestController):
65 65
66 66 def test_returns_list_of_user_groups(self, user_util, xhr_header):
67 67 self.log_user()
68 68 user_group = user_util.create_user_group(active=True)
69 69 user_group_name = user_group.users_group_name
70 70 response = self.app.get(
71 71 route_path('user_group_autocomplete_data'),
72 72 extra_environ=xhr_header, status=200)
73 73 result = json.loads(response.body)
74 74 values = [suggestion['value'] for suggestion in result['suggestions']]
75 75 assert user_group_name in values
76 76
77 77 def test_returns_inactive_user_groups_when_active_flag_sent(
78 78 self, user_util, xhr_header):
79 79 self.log_user()
80 80 user_group = user_util.create_user_group(active=False)
81 81 user_group_name = user_group.users_group_name
82 82
83 83 response = self.app.get(
84 84 route_path('user_group_autocomplete_data',
85 85 params=dict(active='0')),
86 86 extra_environ=xhr_header, status=200)
87 87 result = json.loads(response.body)
88 88 values = [suggestion['value'] for suggestion in result['suggestions']]
89 89 assert user_group_name in values
90 90
91 91 response = self.app.get(
92 92 route_path('user_group_autocomplete_data',
93 93 params=dict(active='1')),
94 94 extra_environ=xhr_header, status=200)
95 95 result = json.loads(response.body)
96 96 values = [suggestion['value'] for suggestion in result['suggestions']]
97 97 assert user_group_name not in values
98 98
99 99 @pytest.mark.parametrize('query, count', [
100 100 ('hello1', 0),
101 101 ('dev', 1),
102 102 ])
103 103 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
104 104 self.log_user()
105 105
106 106 user_util._test_name = 'dev-test'
107 107 user_util.create_user_group()
108 108
109 109 response = self.app.get(
110 110 route_path('user_group_autocomplete_data',
111 111 params=dict(user_groups='true',
112 112 query=query)),
113 113 extra_environ=xhr_header, status=200)
114 114
115 115 result = json.loads(response.body)
116 116
117 117 assert len(result['suggestions']) == count
@@ -1,106 +1,106 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import datetime
22 22
23 23 import pytest
24 24
25 25 from rhodecode.apps._base import ADMIN_PREFIX
26 26 from rhodecode.tests import TestController
27 27 from rhodecode.model.db import UserFollowing, Repository
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32 32
33 33 base_url = {
34 34 'journal': ADMIN_PREFIX + '/journal',
35 35 'journal_rss': ADMIN_PREFIX + '/journal/rss',
36 36 'journal_atom': ADMIN_PREFIX + '/journal/atom',
37 37 'journal_public': ADMIN_PREFIX + '/public_journal',
38 38 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
39 39 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
40 40 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
41 41 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
42 42 'toggle_following': ADMIN_PREFIX + '/toggle_following',
43 43 }[name].format(**kwargs)
44 44
45 45 if params:
46 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 47 return base_url
48 48
49 49
50 50 class TestJournalViews(TestController):
51 51
52 52 def test_journal(self):
53 53 self.log_user()
54 54 response = self.app.get(route_path('journal'))
55 55 # response.mustcontain(
56 56 # """<div class="journal_day">%s</div>""" % datetime.date.today())
57 57
58 58 @pytest.mark.parametrize("feed_type, content_type", [
59 59 ('rss', "application/rss+xml"),
60 60 ('atom', "application/atom+xml")
61 61 ])
62 62 def test_journal_feed(self, feed_type, content_type):
63 63 self.log_user()
64 64 response = self.app.get(
65 65 route_path(
66 66 'journal_{}'.format(feed_type)),
67 67 status=200)
68 68
69 69 assert response.content_type == content_type
70 70
71 71 def test_toggle_following_repository(self, backend):
72 72 user = self.log_user()
73 73 repo = Repository.get_by_repo_name(backend.repo_name)
74 74 repo_id = repo.repo_id
75 75 self.app.post(
76 76 route_path('toggle_following'), {'follows_repo_id': repo_id,
77 77 'csrf_token': self.csrf_token})
78 78
79 79 followings = UserFollowing.query()\
80 80 .filter(UserFollowing.user_id == user['user_id'])\
81 81 .filter(UserFollowing.follows_repo_id == repo_id).all()
82 82
83 83 assert len(followings) == 0
84 84
85 85 self.app.post(
86 86 route_path('toggle_following'), {'follows_repo_id': repo_id,
87 87 'csrf_token': self.csrf_token})
88 88
89 89 followings = UserFollowing.query()\
90 90 .filter(UserFollowing.user_id == user['user_id'])\
91 91 .filter(UserFollowing.follows_repo_id == repo_id).all()
92 92
93 93 assert len(followings) == 1
94 94
95 95 @pytest.mark.parametrize("feed_type, content_type", [
96 96 ('rss', "application/rss+xml"),
97 97 ('atom', "application/atom+xml")
98 98 ])
99 99 def test_public_journal_feed(self, feed_type, content_type):
100 100 self.log_user()
101 101 response = self.app.get(
102 102 route_path(
103 103 'journal_public_{}'.format(feed_type)),
104 104 status=200)
105 105
106 106 assert response.content_type == content_type
@@ -1,580 +1,580 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.tests import (
27 27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 28 no_newline_id_generator)
29 29 from rhodecode.tests.fixture import Fixture
30 30 from rhodecode.lib.auth import check_password
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.model.auth_token import AuthTokenModel
33 33 from rhodecode.model.db import User, Notification, UserApiKeys
34 34 from rhodecode.model.meta import Session
35 35
36 36 fixture = Fixture()
37 37
38 38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39 39
40 40
41 41 def route_path(name, params=None, **kwargs):
42 import urllib
42 import urllib.request, urllib.parse, urllib.error
43 43 from rhodecode.apps._base import ADMIN_PREFIX
44 44
45 45 base_url = {
46 46 'login': ADMIN_PREFIX + '/login',
47 47 'logout': ADMIN_PREFIX + '/logout',
48 48 'register': ADMIN_PREFIX + '/register',
49 49 'reset_password':
50 50 ADMIN_PREFIX + '/password_reset',
51 51 'reset_password_confirmation':
52 52 ADMIN_PREFIX + '/password_reset_confirmation',
53 53
54 54 'admin_permissions_application':
55 55 ADMIN_PREFIX + '/permissions/application',
56 56 'admin_permissions_application_update':
57 57 ADMIN_PREFIX + '/permissions/application/update',
58 58
59 59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60 60
61 61 }[name].format(**kwargs)
62 62
63 63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
64 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
65 65 return base_url
66 66
67 67
68 68 @pytest.mark.usefixtures('app')
69 69 class TestLoginController(object):
70 70 destroy_users = set()
71 71
72 72 @classmethod
73 73 def teardown_class(cls):
74 74 fixture.destroy_users(cls.destroy_users)
75 75
76 76 def teardown_method(self, method):
77 77 for n in Notification.query().all():
78 78 Session().delete(n)
79 79
80 80 Session().commit()
81 81 assert Notification.query().all() == []
82 82
83 83 def test_index(self):
84 84 response = self.app.get(route_path('login'))
85 85 assert response.status == '200 OK'
86 86 # Test response...
87 87
88 88 def test_login_admin_ok(self):
89 89 response = self.app.post(route_path('login'),
90 90 {'username': 'test_admin',
91 91 'password': 'test12'}, status=302)
92 92 response = response.follow()
93 93 session = response.get_session_from_response()
94 94 username = session['rhodecode_user'].get('username')
95 95 assert username == 'test_admin'
96 96 response.mustcontain('logout')
97 97
98 98 def test_login_regular_ok(self):
99 99 response = self.app.post(route_path('login'),
100 100 {'username': 'test_regular',
101 101 'password': 'test12'}, status=302)
102 102
103 103 response = response.follow()
104 104 session = response.get_session_from_response()
105 105 username = session['rhodecode_user'].get('username')
106 106 assert username == 'test_regular'
107 107 response.mustcontain('logout')
108 108
109 109 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 110 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 111 with fixture.auth_restriction(self.app._pyramid_registry,
112 112 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 113 response = self.app.post(route_path('login'),
114 114 {'username': 'test_regular',
115 115 'password': 'test12'})
116 116
117 117 response.mustcontain('invalid user name')
118 118 response.mustcontain('invalid password')
119 119
120 120 def test_login_regular_forbidden_when_scope_restriction(self):
121 121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 122 with fixture.scope_restriction(self.app._pyramid_registry,
123 123 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
124 124 response = self.app.post(route_path('login'),
125 125 {'username': 'test_regular',
126 126 'password': 'test12'})
127 127
128 128 response.mustcontain('invalid user name')
129 129 response.mustcontain('invalid password')
130 130
131 131 def test_login_ok_came_from(self):
132 132 test_came_from = '/_admin/users?branch=stable'
133 133 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
134 134 response = self.app.post(
135 135 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
136 136
137 137 assert 'branch=stable' in response.location
138 138 response = response.follow()
139 139
140 140 assert response.status == '200 OK'
141 141 response.mustcontain('Users administration')
142 142
143 143 def test_redirect_to_login_with_get_args(self):
144 144 with fixture.anon_access(False):
145 145 kwargs = {'branch': 'stable'}
146 146 response = self.app.get(
147 147 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
148 148 status=302)
149 149
150 150 response_query = urlparse.parse_qsl(response.location)
151 151 assert 'branch=stable' in response_query[0][1]
152 152
153 153 def test_login_form_with_get_args(self):
154 154 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
155 155 response = self.app.get(_url)
156 156 assert 'branch%3Dstable' in response.form.action
157 157
158 158 @pytest.mark.parametrize("url_came_from", [
159 159 'data:text/html,<script>window.alert("xss")</script>',
160 160 'mailto:test@rhodecode.org',
161 161 'file:///etc/passwd',
162 162 'ftp://some.ftp.server',
163 163 'http://other.domain',
164 164 '/\r\nX-Forwarded-Host: http://example.org',
165 165 ], ids=no_newline_id_generator)
166 166 def test_login_bad_came_froms(self, url_came_from):
167 167 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
168 168 response = self.app.post(
169 169 _url,
170 170 {'username': 'test_admin', 'password': 'test12'})
171 171 assert response.status == '302 Found'
172 172 response = response.follow()
173 173 assert response.status == '200 OK'
174 174 assert response.request.path == '/'
175 175
176 176 def test_login_short_password(self):
177 177 response = self.app.post(route_path('login'),
178 178 {'username': 'test_admin',
179 179 'password': 'as'})
180 180 assert response.status == '200 OK'
181 181
182 182 response.mustcontain('Enter 3 characters or more')
183 183
184 184 def test_login_wrong_non_ascii_password(self, user_regular):
185 185 response = self.app.post(
186 186 route_path('login'),
187 187 {'username': user_regular.username,
188 188 'password': u'invalid-non-asci\xe4'.encode('utf8')})
189 189
190 190 response.mustcontain('invalid user name')
191 191 response.mustcontain('invalid password')
192 192
193 193 def test_login_with_non_ascii_password(self, user_util):
194 194 password = u'valid-non-ascii\xe4'
195 195 user = user_util.create_user(password=password)
196 196 response = self.app.post(
197 197 route_path('login'),
198 198 {'username': user.username,
199 199 'password': password.encode('utf-8')})
200 200 assert response.status_code == 302
201 201
202 202 def test_login_wrong_username_password(self):
203 203 response = self.app.post(route_path('login'),
204 204 {'username': 'error',
205 205 'password': 'test12'})
206 206
207 207 response.mustcontain('invalid user name')
208 208 response.mustcontain('invalid password')
209 209
210 210 def test_login_admin_ok_password_migration(self, real_crypto_backend):
211 211 from rhodecode.lib import auth
212 212
213 213 # create new user, with sha256 password
214 214 temp_user = 'test_admin_sha256'
215 215 user = fixture.create_user(temp_user)
216 216 user.password = auth._RhodeCodeCryptoSha256().hash_create(
217 217 b'test123')
218 218 Session().add(user)
219 219 Session().commit()
220 220 self.destroy_users.add(temp_user)
221 221 response = self.app.post(route_path('login'),
222 222 {'username': temp_user,
223 223 'password': 'test123'}, status=302)
224 224
225 225 response = response.follow()
226 226 session = response.get_session_from_response()
227 227 username = session['rhodecode_user'].get('username')
228 228 assert username == temp_user
229 229 response.mustcontain('logout')
230 230
231 231 # new password should be bcrypted, after log-in and transfer
232 232 user = User.get_by_username(temp_user)
233 233 assert user.password.startswith('$')
234 234
235 235 # REGISTRATIONS
236 236 def test_register(self):
237 237 response = self.app.get(route_path('register'))
238 238 response.mustcontain('Create an Account')
239 239
240 240 def test_register_err_same_username(self):
241 241 uname = 'test_admin'
242 242 response = self.app.post(
243 243 route_path('register'),
244 244 {
245 245 'username': uname,
246 246 'password': 'test12',
247 247 'password_confirmation': 'test12',
248 248 'email': 'goodmail@domain.com',
249 249 'firstname': 'test',
250 250 'lastname': 'test'
251 251 }
252 252 )
253 253
254 254 assertr = response.assert_response()
255 255 msg = 'Username "%(username)s" already exists'
256 256 msg = msg % {'username': uname}
257 257 assertr.element_contains('#username+.error-message', msg)
258 258
259 259 def test_register_err_same_email(self):
260 260 response = self.app.post(
261 261 route_path('register'),
262 262 {
263 263 'username': 'test_admin_0',
264 264 'password': 'test12',
265 265 'password_confirmation': 'test12',
266 266 'email': 'test_admin@mail.com',
267 267 'firstname': 'test',
268 268 'lastname': 'test'
269 269 }
270 270 )
271 271
272 272 assertr = response.assert_response()
273 273 msg = u'This e-mail address is already taken'
274 274 assertr.element_contains('#email+.error-message', msg)
275 275
276 276 def test_register_err_same_email_case_sensitive(self):
277 277 response = self.app.post(
278 278 route_path('register'),
279 279 {
280 280 'username': 'test_admin_1',
281 281 'password': 'test12',
282 282 'password_confirmation': 'test12',
283 283 'email': 'TesT_Admin@mail.COM',
284 284 'firstname': 'test',
285 285 'lastname': 'test'
286 286 }
287 287 )
288 288 assertr = response.assert_response()
289 289 msg = u'This e-mail address is already taken'
290 290 assertr.element_contains('#email+.error-message', msg)
291 291
292 292 def test_register_err_wrong_data(self):
293 293 response = self.app.post(
294 294 route_path('register'),
295 295 {
296 296 'username': 'xs',
297 297 'password': 'test',
298 298 'password_confirmation': 'test',
299 299 'email': 'goodmailm',
300 300 'firstname': 'test',
301 301 'lastname': 'test'
302 302 }
303 303 )
304 304 assert response.status == '200 OK'
305 305 response.mustcontain('An email address must contain a single @')
306 306 response.mustcontain('Enter a value 6 characters long or more')
307 307
308 308 def test_register_err_username(self):
309 309 response = self.app.post(
310 310 route_path('register'),
311 311 {
312 312 'username': 'error user',
313 313 'password': 'test12',
314 314 'password_confirmation': 'test12',
315 315 'email': 'goodmailm',
316 316 'firstname': 'test',
317 317 'lastname': 'test'
318 318 }
319 319 )
320 320
321 321 response.mustcontain('An email address must contain a single @')
322 322 response.mustcontain(
323 323 'Username may only contain '
324 324 'alphanumeric characters underscores, '
325 325 'periods or dashes and must begin with '
326 326 'alphanumeric character')
327 327
328 328 def test_register_err_case_sensitive(self):
329 329 usr = 'Test_Admin'
330 330 response = self.app.post(
331 331 route_path('register'),
332 332 {
333 333 'username': usr,
334 334 'password': 'test12',
335 335 'password_confirmation': 'test12',
336 336 'email': 'goodmailm',
337 337 'firstname': 'test',
338 338 'lastname': 'test'
339 339 }
340 340 )
341 341
342 342 assertr = response.assert_response()
343 343 msg = u'Username "%(username)s" already exists'
344 344 msg = msg % {'username': usr}
345 345 assertr.element_contains('#username+.error-message', msg)
346 346
347 347 def test_register_special_chars(self):
348 348 response = self.app.post(
349 349 route_path('register'),
350 350 {
351 351 'username': 'xxxaxn',
352 352 'password': 'ąćźżąśśśś',
353 353 'password_confirmation': 'ąćźżąśśśś',
354 354 'email': 'goodmailm@test.plx',
355 355 'firstname': 'test',
356 356 'lastname': 'test'
357 357 }
358 358 )
359 359
360 360 msg = u'Invalid characters (non-ascii) in password'
361 361 response.mustcontain(msg)
362 362
363 363 def test_register_password_mismatch(self):
364 364 response = self.app.post(
365 365 route_path('register'),
366 366 {
367 367 'username': 'xs',
368 368 'password': '123qwe',
369 369 'password_confirmation': 'qwe123',
370 370 'email': 'goodmailm@test.plxa',
371 371 'firstname': 'test',
372 372 'lastname': 'test'
373 373 }
374 374 )
375 375 msg = u'Passwords do not match'
376 376 response.mustcontain(msg)
377 377
378 378 def test_register_ok(self):
379 379 username = 'test_regular4'
380 380 password = 'qweqwe'
381 381 email = 'marcin@test.com'
382 382 name = 'testname'
383 383 lastname = 'testlastname'
384 384
385 385 # this initializes a session
386 386 response = self.app.get(route_path('register'))
387 387 response.mustcontain('Create an Account')
388 388
389 389
390 390 response = self.app.post(
391 391 route_path('register'),
392 392 {
393 393 'username': username,
394 394 'password': password,
395 395 'password_confirmation': password,
396 396 'email': email,
397 397 'firstname': name,
398 398 'lastname': lastname,
399 399 'admin': True
400 400 },
401 401 status=302
402 402 ) # This should be overridden
403 403
404 404 assert_session_flash(
405 405 response, 'You have successfully registered with RhodeCode. You can log-in now.')
406 406
407 407 ret = Session().query(User).filter(
408 408 User.username == 'test_regular4').one()
409 409 assert ret.username == username
410 410 assert check_password(password, ret.password)
411 411 assert ret.email == email
412 412 assert ret.name == name
413 413 assert ret.lastname == lastname
414 414 assert ret.auth_tokens is not None
415 415 assert not ret.admin
416 416
417 417 def test_forgot_password_wrong_mail(self):
418 418 bad_email = 'marcin@wrongmail.org'
419 419 # this initializes a session
420 420 self.app.get(route_path('reset_password'))
421 421
422 422 response = self.app.post(
423 423 route_path('reset_password'), {'email': bad_email, }
424 424 )
425 425 assert_session_flash(response,
426 426 'If such email exists, a password reset link was sent to it.')
427 427
428 428 def test_forgot_password(self, user_util):
429 429 # this initializes a session
430 430 self.app.get(route_path('reset_password'))
431 431
432 432 user = user_util.create_user()
433 433 user_id = user.user_id
434 434 email = user.email
435 435
436 436 response = self.app.post(route_path('reset_password'), {'email': email, })
437 437
438 438 assert_session_flash(response,
439 439 'If such email exists, a password reset link was sent to it.')
440 440
441 441 # BAD KEY
442 442 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
443 443 response = self.app.get(confirm_url, status=302)
444 444 assert response.location.endswith(route_path('reset_password'))
445 445 assert_session_flash(response, 'Given reset token is invalid')
446 446
447 447 response.follow() # cleanup flash
448 448
449 449 # GOOD KEY
450 450 key = UserApiKeys.query()\
451 451 .filter(UserApiKeys.user_id == user_id)\
452 452 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
453 453 .first()
454 454
455 455 assert key
456 456
457 457 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
458 458 response = self.app.get(confirm_url)
459 459 assert response.status == '302 Found'
460 460 assert response.location.endswith(route_path('login'))
461 461
462 462 assert_session_flash(
463 463 response,
464 464 'Your password reset was successful, '
465 465 'a new password has been sent to your email')
466 466
467 467 response.follow()
468 468
469 469 def _get_api_whitelist(self, values=None):
470 470 config = {'api_access_controllers_whitelist': values or []}
471 471 return config
472 472
473 473 @pytest.mark.parametrize("test_name, auth_token", [
474 474 ('none', None),
475 475 ('empty_string', ''),
476 476 ('fake_number', '123456'),
477 477 ('proper_auth_token', None)
478 478 ])
479 479 def test_access_not_whitelisted_page_via_auth_token(
480 480 self, test_name, auth_token, user_admin):
481 481
482 482 whitelist = self._get_api_whitelist([])
483 483 with mock.patch.dict('rhodecode.CONFIG', whitelist):
484 484 assert [] == whitelist['api_access_controllers_whitelist']
485 485 if test_name == 'proper_auth_token':
486 486 # use builtin if api_key is None
487 487 auth_token = user_admin.api_key
488 488
489 489 with fixture.anon_access(False):
490 490 self.app.get(
491 491 route_path('repo_commit_raw',
492 492 repo_name=HG_REPO, commit_id='tip',
493 493 params=dict(api_key=auth_token)),
494 494 status=302)
495 495
496 496 @pytest.mark.parametrize("test_name, auth_token, code", [
497 497 ('none', None, 302),
498 498 ('empty_string', '', 302),
499 499 ('fake_number', '123456', 302),
500 500 ('proper_auth_token', None, 200)
501 501 ])
502 502 def test_access_whitelisted_page_via_auth_token(
503 503 self, test_name, auth_token, code, user_admin):
504 504
505 505 whitelist = self._get_api_whitelist(whitelist_view)
506 506
507 507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
508 508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
509 509
510 510 if test_name == 'proper_auth_token':
511 511 auth_token = user_admin.api_key
512 512 assert auth_token
513 513
514 514 with fixture.anon_access(False):
515 515 self.app.get(
516 516 route_path('repo_commit_raw',
517 517 repo_name=HG_REPO, commit_id='tip',
518 518 params=dict(api_key=auth_token)),
519 519 status=code)
520 520
521 521 @pytest.mark.parametrize("test_name, auth_token, code", [
522 522 ('proper_auth_token', None, 200),
523 523 ('wrong_auth_token', '123456', 302),
524 524 ])
525 525 def test_access_whitelisted_page_via_auth_token_bound_to_token(
526 526 self, test_name, auth_token, code, user_admin):
527 527
528 528 expected_token = auth_token
529 529 if test_name == 'proper_auth_token':
530 530 auth_token = user_admin.api_key
531 531 expected_token = auth_token
532 532 assert auth_token
533 533
534 534 whitelist = self._get_api_whitelist([
535 535 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
536 536
537 537 with mock.patch.dict('rhodecode.CONFIG', whitelist):
538 538
539 539 with fixture.anon_access(False):
540 540 self.app.get(
541 541 route_path('repo_commit_raw',
542 542 repo_name=HG_REPO, commit_id='tip',
543 543 params=dict(api_key=auth_token)),
544 544 status=code)
545 545
546 546 def test_access_page_via_extra_auth_token(self):
547 547 whitelist = self._get_api_whitelist(whitelist_view)
548 548 with mock.patch.dict('rhodecode.CONFIG', whitelist):
549 549 assert whitelist_view == \
550 550 whitelist['api_access_controllers_whitelist']
551 551
552 552 new_auth_token = AuthTokenModel().create(
553 553 TEST_USER_ADMIN_LOGIN, 'test')
554 554 Session().commit()
555 555 with fixture.anon_access(False):
556 556 self.app.get(
557 557 route_path('repo_commit_raw',
558 558 repo_name=HG_REPO, commit_id='tip',
559 559 params=dict(api_key=new_auth_token.api_key)),
560 560 status=200)
561 561
562 562 def test_access_page_via_expired_auth_token(self):
563 563 whitelist = self._get_api_whitelist(whitelist_view)
564 564 with mock.patch.dict('rhodecode.CONFIG', whitelist):
565 565 assert whitelist_view == \
566 566 whitelist['api_access_controllers_whitelist']
567 567
568 568 new_auth_token = AuthTokenModel().create(
569 569 TEST_USER_ADMIN_LOGIN, 'test')
570 570 Session().commit()
571 571 # patch the api key and make it expired
572 572 new_auth_token.expires = 0
573 573 Session().add(new_auth_token)
574 574 Session().commit()
575 575 with fixture.anon_access(False):
576 576 self.app.get(
577 577 route_path('repo_commit_raw',
578 578 repo_name=HG_REPO, commit_id='tip',
579 579 params=dict(api_key=new_auth_token.api_key)),
580 580 status=302)
@@ -1,118 +1,118 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.lib import helpers as h
24 24 from rhodecode.tests import (
25 25 TestController, clear_cache_regions,
26 26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 27 from rhodecode.tests.fixture import Fixture
28 28 from rhodecode.tests.utils import AssertResponse
29 29
30 30 fixture = Fixture()
31 31
32 32
33 33 def route_path(name, params=None, **kwargs):
34 import urllib
34 import urllib.request, urllib.parse, urllib.error
35 35 from rhodecode.apps._base import ADMIN_PREFIX
36 36
37 37 base_url = {
38 38 'login': ADMIN_PREFIX + '/login',
39 39 'logout': ADMIN_PREFIX + '/logout',
40 40 'register': ADMIN_PREFIX + '/register',
41 41 'reset_password':
42 42 ADMIN_PREFIX + '/password_reset',
43 43 'reset_password_confirmation':
44 44 ADMIN_PREFIX + '/password_reset_confirmation',
45 45
46 46 'admin_permissions_application':
47 47 ADMIN_PREFIX + '/permissions/application',
48 48 'admin_permissions_application_update':
49 49 ADMIN_PREFIX + '/permissions/application/update',
50 50 }[name].format(**kwargs)
51 51
52 52 if params:
53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 54 return base_url
55 55
56 56
57 57 class TestPasswordReset(TestController):
58 58
59 59 @pytest.mark.parametrize(
60 60 'pwd_reset_setting, show_link, show_reset', [
61 61 ('hg.password_reset.enabled', True, True),
62 62 ('hg.password_reset.hidden', False, True),
63 63 ('hg.password_reset.disabled', False, False),
64 64 ])
65 65 def test_password_reset_settings(
66 66 self, pwd_reset_setting, show_link, show_reset):
67 67 clear_cache_regions()
68 68 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 69 params = {
70 70 'csrf_token': self.csrf_token,
71 71 'anonymous': 'True',
72 72 'default_register': 'hg.register.auto_activate',
73 73 'default_register_message': '',
74 74 'default_password_reset': pwd_reset_setting,
75 75 'default_extern_activate': 'hg.extern_activate.auto',
76 76 }
77 77 resp = self.app.post(
78 78 route_path('admin_permissions_application_update'), params=params)
79 79 self.logout_user()
80 80
81 81 login_page = self.app.get(route_path('login'))
82 82 asr_login = AssertResponse(login_page)
83 83
84 84 if show_link:
85 85 asr_login.one_element_exists('a.pwd_reset')
86 86 else:
87 87 asr_login.no_element_exists('a.pwd_reset')
88 88
89 89 response = self.app.get(route_path('reset_password'))
90 90
91 91 assert_response = response.assert_response()
92 92 if show_reset:
93 93 response.mustcontain('Send password reset email')
94 94 assert_response.one_element_exists('#email')
95 95 assert_response.one_element_exists('#send')
96 96 else:
97 97 response.mustcontain('Password reset is disabled.')
98 98 assert_response.no_element_exists('#email')
99 99 assert_response.no_element_exists('#send')
100 100
101 101 def test_password_form_disabled(self):
102 102 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
103 103 params = {
104 104 'csrf_token': self.csrf_token,
105 105 'anonymous': 'True',
106 106 'default_register': 'hg.register.auto_activate',
107 107 'default_register_message': '',
108 108 'default_password_reset': 'hg.password_reset.disabled',
109 109 'default_extern_activate': 'hg.extern_activate.auto',
110 110 }
111 111 self.app.post(route_path('admin_permissions_application_update'), params=params)
112 112 self.logout_user()
113 113
114 114 response = self.app.post(
115 115 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
116 116 )
117 117 response = response.follow()
118 118 response.mustcontain('Password reset is disabled.')
@@ -1,208 +1,208 b''
1 1 # -*- coding: utf-8 -*-
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 # -*- coding: utf-8 -*-
21 21
22 22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 23 #
24 24 # This program is free software: you can redistribute it and/or modify
25 25 # it under the terms of the GNU Affero General Public License, version 3
26 26 # (only), as published by the Free Software Foundation.
27 27 #
28 28 # This program is distributed in the hope that it will be useful,
29 29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 31 # GNU General Public License for more details.
32 32 #
33 33 # You should have received a copy of the GNU Affero General Public License
34 34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 35 #
36 36 # This program is dual-licensed. If you wish to learn more about the
37 37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39 39
40 40 import pytest
41 41
42 42 from rhodecode.model.db import User
43 43 from rhodecode.tests import TestController, assert_session_flash
44 44 from rhodecode.lib import helpers as h
45 45
46 46
47 47 def route_path(name, params=None, **kwargs):
48 import urllib
48 import urllib.request, urllib.parse, urllib.error
49 49 from rhodecode.apps._base import ADMIN_PREFIX
50 50
51 51 base_url = {
52 52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 56 }[name].format(**kwargs)
57 57
58 58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 60 return base_url
61 61
62 62
63 63 class TestMyAccountEdit(TestController):
64 64
65 65 def test_my_account_edit(self):
66 66 self.log_user()
67 67 response = self.app.get(route_path('my_account_edit'))
68 68
69 69 response.mustcontain('value="test_admin')
70 70
71 71 @pytest.mark.backends("git", "hg")
72 72 def test_my_account_my_pullrequests(self, pr_util):
73 73 self.log_user()
74 74 response = self.app.get(route_path('my_account_pullrequests'))
75 75 response.mustcontain('There are currently no open pull '
76 76 'requests requiring your participation.')
77 77
78 78 @pytest.mark.backends("git", "hg")
79 79 @pytest.mark.parametrize('params, expected_title', [
80 80 ({'closed': 1}, 'Closed'),
81 81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 82 ])
83 83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
84 84 self.log_user()
85 85 response = self.app.get(route_path('my_account_pullrequests_data'),
86 86 extra_environ=xhr_header)
87 87 assert response.json == {
88 88 u'data': [], u'draw': None,
89 89 u'recordsFiltered': 0, u'recordsTotal': 0}
90 90
91 91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 92 expected = {
93 93 'author_raw': 'RhodeCode Admin',
94 94 'name_raw': pr.pull_request_id
95 95 }
96 96 response = self.app.get(route_path('my_account_pullrequests_data'),
97 97 extra_environ=xhr_header)
98 98 assert response.json['recordsTotal'] == 1
99 99 assert response.json['data'][0]['author_raw'] == expected['author_raw']
100 100
101 101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
102 102 assert response.json['data'][0]['name_raw'] == expected['name_raw']
103 103
104 104 @pytest.mark.parametrize(
105 105 "name, attrs", [
106 106 ('firstname', {'firstname': 'new_username'}),
107 107 ('lastname', {'lastname': 'new_username'}),
108 108 ('admin', {'admin': True}),
109 109 ('admin', {'admin': False}),
110 110 ('extern_type', {'extern_type': 'ldap'}),
111 111 ('extern_type', {'extern_type': None}),
112 112 # ('extern_name', {'extern_name': 'test'}),
113 113 # ('extern_name', {'extern_name': None}),
114 114 ('active', {'active': False}),
115 115 ('active', {'active': True}),
116 116 ('email', {'email': u'some@email.com'}),
117 117 ])
118 118 def test_my_account_update(self, name, attrs, user_util):
119 119 usr = user_util.create_user(password='qweqwe')
120 120 params = usr.get_api_data() # current user data
121 121 user_id = usr.user_id
122 122 self.log_user(
123 123 username=usr.username, password='qweqwe')
124 124
125 125 params.update({'password_confirmation': ''})
126 126 params.update({'new_password': ''})
127 127 params.update({'extern_type': u'rhodecode'})
128 128 params.update({'extern_name': u'rhodecode'})
129 129 params.update({'csrf_token': self.csrf_token})
130 130
131 131 params.update(attrs)
132 132 # my account page cannot set language param yet, only for admins
133 133 del params['language']
134 134 if name == 'email':
135 135 uem = user_util.create_additional_user_email(usr, attrs['email'])
136 136 email_before = User.get(user_id).email
137 137
138 138 response = self.app.post(route_path('my_account_update'), params)
139 139
140 140 assert_session_flash(
141 141 response, 'Your account was updated successfully')
142 142
143 143 del params['csrf_token']
144 144
145 145 updated_user = User.get(user_id)
146 146 updated_params = updated_user.get_api_data()
147 147 updated_params.update({'password_confirmation': ''})
148 148 updated_params.update({'new_password': ''})
149 149
150 150 params['last_login'] = updated_params['last_login']
151 151 params['last_activity'] = updated_params['last_activity']
152 152 # my account page cannot set language param yet, only for admins
153 153 # but we get this info from API anyway
154 154 params['language'] = updated_params['language']
155 155
156 156 if name == 'email':
157 157 params['emails'] = [attrs['email'], email_before]
158 158 if name == 'extern_type':
159 159 # cannot update this via form, expected value is original one
160 160 params['extern_type'] = "rhodecode"
161 161 if name == 'extern_name':
162 162 # cannot update this via form, expected value is original one
163 163 params['extern_name'] = str(user_id)
164 164 if name == 'active':
165 165 # my account cannot deactivate account
166 166 params['active'] = True
167 167 if name == 'admin':
168 168 # my account cannot make you an admin !
169 169 params['admin'] = False
170 170
171 171 assert params == updated_params
172 172
173 173 def test_my_account_update_err_email_not_exists_in_emails(self):
174 174 self.log_user()
175 175
176 176 new_email = 'test_regular@mail.com' # not in emails
177 177 params = {
178 178 'username': 'test_admin',
179 179 'new_password': 'test12',
180 180 'password_confirmation': 'test122',
181 181 'firstname': 'NewName',
182 182 'lastname': 'NewLastname',
183 183 'email': new_email,
184 184 'csrf_token': self.csrf_token,
185 185 }
186 186
187 187 response = self.app.post(route_path('my_account_update'),
188 188 params=params)
189 189
190 190 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
191 191
192 192 def test_my_account_update_bad_email_address(self):
193 193 self.log_user('test_regular2', 'test12')
194 194
195 195 new_email = 'newmail.pl'
196 196 params = {
197 197 'username': 'test_admin',
198 198 'new_password': 'test12',
199 199 'password_confirmation': 'test122',
200 200 'firstname': 'NewName',
201 201 'lastname': 'NewLastname',
202 202 'email': new_email,
203 203 'csrf_token': self.csrf_token,
204 204 }
205 205 response = self.app.post(route_path('my_account_update'),
206 206 params=params)
207 207
208 208 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,206 +1,206 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.tests import (
25 25 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
26 26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 from rhodecode.model.db import Notification, User
30 30 from rhodecode.model.user import UserModel
31 31 from rhodecode.model.notification import NotificationModel
32 32 from rhodecode.model.meta import Session
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39 39 from rhodecode.apps._base import ADMIN_PREFIX
40 40
41 41 base_url = {
42 42 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
44 44 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 45 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 46 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47 47
48 48 }[name].format(**kwargs)
49 49
50 50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 52 return base_url
53 53
54 54
55 55 class TestNotificationsController(TestController):
56 56
57 57 def teardown_method(self, method):
58 58 for n in Notification.query().all():
59 59 inst = Notification.get(n.notification_id)
60 60 Session().delete(inst)
61 61 Session().commit()
62 62
63 63 def test_mark_all_read(self, user_util):
64 64 user = user_util.create_user(password='qweqwe')
65 65 self.log_user(user.username, 'qweqwe')
66 66
67 67 self.app.post(
68 68 route_path('notifications_mark_all_read'), status=302,
69 69 params={'csrf_token': self.csrf_token}
70 70 )
71 71
72 72 def test_show_all(self, user_util):
73 73 user = user_util.create_user(password='qweqwe')
74 74 user_id = user.user_id
75 75 self.log_user(user.username, 'qweqwe')
76 76
77 77 response = self.app.get(
78 78 route_path('notifications_show_all', params={'type': 'all'}))
79 79 response.mustcontain(
80 80 '<div class="table">No notifications here yet</div>')
81 81
82 82 notification = NotificationModel().create(
83 83 created_by=user_id, notification_subject=u'test_notification_1',
84 84 notification_body=u'notification_1', recipients=[user_id])
85 85 Session().commit()
86 86 notification_id = notification.notification_id
87 87
88 88 response = self.app.get(route_path('notifications_show_all',
89 89 params={'type': 'all'}))
90 90 response.mustcontain('id="notification_%s"' % notification_id)
91 91
92 92 def test_show_unread(self, user_util):
93 93 user = user_util.create_user(password='qweqwe')
94 94 user_id = user.user_id
95 95 self.log_user(user.username, 'qweqwe')
96 96
97 97 response = self.app.get(route_path('notifications_show_all'))
98 98 response.mustcontain(
99 99 '<div class="table">No notifications here yet</div>')
100 100
101 101 notification = NotificationModel().create(
102 102 created_by=user_id, notification_subject=u'test_notification_1',
103 103 notification_body=u'notification_1', recipients=[user_id])
104 104
105 105 # mark the USER notification as unread
106 106 user_notification = NotificationModel().get_user_notification(
107 107 user_id, notification)
108 108 user_notification.read = False
109 109
110 110 Session().commit()
111 111 notification_id = notification.notification_id
112 112
113 113 response = self.app.get(route_path('notifications_show_all'))
114 114 response.mustcontain('id="notification_%s"' % notification_id)
115 115 response.mustcontain('<div class="desc unread')
116 116
117 117 @pytest.mark.parametrize('user,password', [
118 118 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
119 119 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
120 120 ])
121 121 def test_delete(self, user, password, user_util):
122 122 self.log_user(user, password)
123 123 cur_user = self._get_logged_user()
124 124
125 125 u1 = user_util.create_user()
126 126 u2 = user_util.create_user()
127 127
128 128 # make notifications
129 129 notification = NotificationModel().create(
130 130 created_by=cur_user, notification_subject=u'test',
131 131 notification_body=u'hi there', recipients=[cur_user, u1, u2])
132 132 Session().commit()
133 133 u1 = User.get(u1.user_id)
134 134 u2 = User.get(u2.user_id)
135 135
136 136 # check DB
137 137 get_notif = lambda un: [x.notification for x in un]
138 138 assert get_notif(cur_user.notifications) == [notification]
139 139 assert get_notif(u1.notifications) == [notification]
140 140 assert get_notif(u2.notifications) == [notification]
141 141 cur_usr_id = cur_user.user_id
142 142
143 143 response = self.app.post(
144 144 route_path('notifications_delete',
145 145 notification_id=notification.notification_id),
146 146 params={'csrf_token': self.csrf_token})
147 147 assert response.json == 'ok'
148 148
149 149 cur_user = User.get(cur_usr_id)
150 150 assert cur_user.notifications == []
151 151
152 152 @pytest.mark.parametrize('user,password', [
153 153 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
154 154 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
155 155 ])
156 156 def test_show(self, user, password, user_util):
157 157 self.log_user(user, password)
158 158 cur_user = self._get_logged_user()
159 159 u1 = user_util.create_user()
160 160 u2 = user_util.create_user()
161 161
162 162 subject = u'test'
163 163 notif_body = u'hi there'
164 164 notification = NotificationModel().create(
165 165 created_by=cur_user, notification_subject=subject,
166 166 notification_body=notif_body, recipients=[cur_user, u1, u2])
167 167 Session().commit()
168 168
169 169 response = self.app.get(
170 170 route_path('notifications_show',
171 171 notification_id=notification.notification_id))
172 172
173 173 response.mustcontain(subject)
174 174 response.mustcontain(notif_body)
175 175
176 176 @pytest.mark.parametrize('user,password', [
177 177 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
178 178 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
179 179 ])
180 180 def test_update(self, user, password, user_util):
181 181 self.log_user(user, password)
182 182 cur_user = self._get_logged_user()
183 183 u1 = user_util.create_user()
184 184 u2 = user_util.create_user()
185 185
186 186 # make notifications
187 187 recipients = [cur_user, u1, u2]
188 188 notification = NotificationModel().create(
189 189 created_by=cur_user, notification_subject=u'test',
190 190 notification_body=u'hi there', recipients=recipients)
191 191 Session().commit()
192 192
193 193 for u_obj in recipients:
194 194 # if it's current user, he has his message already read
195 195 read = u_obj.username == user
196 196 assert len(u_obj.notifications) == 1
197 197 assert u_obj.notifications[0].read == read
198 198
199 199 response = self.app.post(
200 200 route_path('notifications_update',
201 201 notification_id=notification.notification_id),
202 202 params={'csrf_token': self.csrf_token})
203 203 assert response.json == 'ok'
204 204
205 205 cur_user = self._get_logged_user()
206 206 assert True is cur_user.notifications[0].read
@@ -1,163 +1,163 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.model.db import User, UserSshKeys
24 24
25 25 from rhodecode.tests import TestController, assert_session_flash
26 26 from rhodecode.tests.fixture import Fixture
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 33 from rhodecode.apps._base import ADMIN_PREFIX
34 34
35 35 base_url = {
36 36 'my_account_ssh_keys':
37 37 ADMIN_PREFIX + '/my_account/ssh_keys',
38 38 'my_account_ssh_keys_generate':
39 39 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
40 40 'my_account_ssh_keys_add':
41 41 ADMIN_PREFIX + '/my_account/ssh_keys/new',
42 42 'my_account_ssh_keys_delete':
43 43 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
44 44 }[name].format(**kwargs)
45 45
46 46 if params:
47 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 48 return base_url
49 49
50 50
51 51 class TestMyAccountSshKeysView(TestController):
52 52 INVALID_KEY = """\
53 53 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
54 54 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
55 55 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
56 56 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
57 57 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
58 58 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
59 59 your_email@example.com
60 60 """
61 61 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
62 62 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
63 63 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
64 64 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
65 65 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
66 66 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
67 67 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
68 68 'your_email@example.com'
69 69 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
70 70
71 71 def test_add_ssh_key_error(self, user_util):
72 72 user = user_util.create_user(password='qweqwe')
73 73 self.log_user(user.username, 'qweqwe')
74 74
75 75 key_data = self.INVALID_KEY
76 76
77 77 desc = 'MY SSH KEY'
78 78 response = self.app.post(
79 79 route_path('my_account_ssh_keys_add'),
80 80 {'description': desc, 'key_data': key_data,
81 81 'csrf_token': self.csrf_token})
82 82 assert_session_flash(response, 'An error occurred during ssh '
83 83 'key saving: Unable to decode the key')
84 84
85 85 def test_ssh_key_duplicate(self, user_util):
86 86 user = user_util.create_user(password='qweqwe')
87 87 self.log_user(user.username, 'qweqwe')
88 88 key_data = self.VALID_KEY
89 89
90 90 desc = 'MY SSH KEY'
91 91 response = self.app.post(
92 92 route_path('my_account_ssh_keys_add'),
93 93 {'description': desc, 'key_data': key_data,
94 94 'csrf_token': self.csrf_token})
95 95 assert_session_flash(response, 'Ssh Key successfully created')
96 96 response.follow() # flush session flash
97 97
98 98 # add the same key AGAIN
99 99 desc = 'MY SSH KEY'
100 100 response = self.app.post(
101 101 route_path('my_account_ssh_keys_add'),
102 102 {'description': desc, 'key_data': key_data,
103 103 'csrf_token': self.csrf_token})
104 104
105 105 err = 'Such key with fingerprint `{}` already exists, ' \
106 106 'please use a different one'.format(self.FINGERPRINT)
107 107 assert_session_flash(response, 'An error occurred during ssh key '
108 108 'saving: {}'.format(err))
109 109
110 110 def test_add_ssh_key(self, user_util):
111 111 user = user_util.create_user(password='qweqwe')
112 112 self.log_user(user.username, 'qweqwe')
113 113
114 114 key_data = self.VALID_KEY
115 115
116 116 desc = 'MY SSH KEY'
117 117 response = self.app.post(
118 118 route_path('my_account_ssh_keys_add'),
119 119 {'description': desc, 'key_data': key_data,
120 120 'csrf_token': self.csrf_token})
121 121 assert_session_flash(response, 'Ssh Key successfully created')
122 122
123 123 response = response.follow()
124 124 response.mustcontain(desc)
125 125
126 126 def test_delete_ssh_key(self, user_util):
127 127 user = user_util.create_user(password='qweqwe')
128 128 user_id = user.user_id
129 129 self.log_user(user.username, 'qweqwe')
130 130
131 131 key_data = self.VALID_KEY
132 132
133 133 desc = 'MY SSH KEY'
134 134 response = self.app.post(
135 135 route_path('my_account_ssh_keys_add'),
136 136 {'description': desc, 'key_data': key_data,
137 137 'csrf_token': self.csrf_token})
138 138 assert_session_flash(response, 'Ssh Key successfully created')
139 139 response = response.follow() # flush the Session flash
140 140
141 141 # now delete our key
142 142 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
143 143 assert 1 == len(keys)
144 144
145 145 response = self.app.post(
146 146 route_path('my_account_ssh_keys_delete'),
147 147 {'del_ssh_key': keys[0].ssh_key_id,
148 148 'csrf_token': self.csrf_token})
149 149
150 150 assert_session_flash(response, 'Ssh key successfully deleted')
151 151 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
152 152 assert 0 == len(keys)
153 153
154 154 def test_generate_keypair(self, user_util):
155 155 user = user_util.create_user(password='qweqwe')
156 156 self.log_user(user.username, 'qweqwe')
157 157
158 158 response = self.app.get(
159 159 route_path('my_account_ssh_keys_generate'))
160 160
161 161 response.mustcontain('Private key')
162 162 response.mustcontain('Public key')
163 163 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,89 +1,89 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests import assert_session_flash
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28 28
29 29 base_url = {
30 30 'edit_repo_group_advanced':
31 31 '/{repo_group_name}/_settings/advanced',
32 32 'edit_repo_group_advanced_delete':
33 33 '/{repo_group_name}/_settings/advanced/delete',
34 34 }[name].format(**kwargs)
35 35
36 36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 38 return base_url
39 39
40 40
41 41 @pytest.mark.usefixtures("app")
42 42 class TestRepoGroupsAdvancedView(object):
43 43
44 44 @pytest.mark.parametrize('repo_group_name', [
45 45 'gro',
46 46 '12345',
47 47 ])
48 48 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
49 49 user_util._test_name = repo_group_name
50 50 gr = user_util.create_repo_group()
51 51 self.app.get(
52 52 route_path('edit_repo_group_advanced',
53 53 repo_group_name=gr.group_name))
54 54
55 55 def test_show_advanced_settings_delete(self, autologin_user, user_util,
56 56 csrf_token):
57 57 gr = user_util.create_repo_group(auto_cleanup=False)
58 58 repo_group_name = gr.group_name
59 59
60 60 params = dict(
61 61 csrf_token=csrf_token
62 62 )
63 63 response = self.app.post(
64 64 route_path('edit_repo_group_advanced_delete',
65 65 repo_group_name=repo_group_name), params=params)
66 66 assert_session_flash(
67 67 response, 'Removed repository group `{}`'.format(repo_group_name))
68 68
69 69 def test_delete_not_possible_with_objects_inside(self, autologin_user,
70 70 repo_groups, csrf_token):
71 71 zombie_group, parent_group, child_group = repo_groups
72 72
73 73 response = self.app.get(
74 74 route_path('edit_repo_group_advanced',
75 75 repo_group_name=parent_group.group_name))
76 76
77 77 response.mustcontain(
78 78 'This repository group includes 1 children repository group')
79 79
80 80 params = dict(
81 81 csrf_token=csrf_token
82 82 )
83 83 response = self.app.post(
84 84 route_path('edit_repo_group_advanced_delete',
85 85 repo_group_name=parent_group.group_name), params=params)
86 86
87 87 assert_session_flash(
88 88 response, 'This repository group contains 1 subgroup '
89 89 'and cannot be deleted')
@@ -1,86 +1,86 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests.utils import permission_update_data_generator
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28 28
29 29 base_url = {
30 30 'edit_repo_group_perms':
31 31 '/{repo_group_name:}/_settings/permissions',
32 32 'edit_repo_group_perms_update':
33 33 '/{repo_group_name}/_settings/permissions/update',
34 34 }[name].format(**kwargs)
35 35
36 36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 38 return base_url
39 39
40 40
41 41 @pytest.mark.usefixtures("app")
42 42 class TestRepoGroupPermissionsView(object):
43 43
44 44 def test_edit_perms_view(self, user_util, autologin_user):
45 45 repo_group = user_util.create_repo_group()
46 46
47 47 self.app.get(
48 48 route_path('edit_repo_group_perms',
49 49 repo_group_name=repo_group.group_name), status=200)
50 50
51 51 def test_update_permissions(self, csrf_token, user_util):
52 52 repo_group = user_util.create_repo_group()
53 53 repo_group_name = repo_group.group_name
54 54 user = user_util.create_user()
55 55 user_id = user.user_id
56 56 username = user.username
57 57
58 58 # grant new
59 59 form_data = permission_update_data_generator(
60 60 csrf_token,
61 61 default='group.write',
62 62 grant=[(user_id, 'group.write', username, 'user')])
63 63
64 64 # recursive flag required for repo groups
65 65 form_data.extend([('recursive', u'none')])
66 66
67 67 response = self.app.post(
68 68 route_path('edit_repo_group_perms_update',
69 69 repo_group_name=repo_group_name), form_data).follow()
70 70
71 71 assert 'Repository Group permissions updated' in response
72 72
73 73 # revoke given
74 74 form_data = permission_update_data_generator(
75 75 csrf_token,
76 76 default='group.read',
77 77 revoke=[(user_id, 'user')])
78 78
79 79 # recursive flag required for repo groups
80 80 form_data.extend([('recursive', u'none')])
81 81
82 82 response = self.app.post(
83 83 route_path('edit_repo_group_perms_update',
84 84 repo_group_name=repo_group_name), form_data).follow()
85 85
86 86 assert 'Repository Group permissions updated' in response
@@ -1,90 +1,90 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests import assert_session_flash
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28 28
29 29 base_url = {
30 30 'edit_repo_group': '/{repo_group_name}/_edit',
31 31 # Update is POST to the above url
32 32 }[name].format(**kwargs)
33 33
34 34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 36 return base_url
37 37
38 38
39 39 @pytest.mark.usefixtures("app")
40 40 class TestRepoGroupsSettingsView(object):
41 41
42 42 @pytest.mark.parametrize('repo_group_name', [
43 43 'gro',
44 44 u'12345',
45 45 ])
46 46 def test_edit(self, user_util, autologin_user, repo_group_name):
47 47 user_util._test_name = repo_group_name
48 48 repo_group = user_util.create_repo_group()
49 49
50 50 self.app.get(
51 51 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
52 52 status=200)
53 53
54 54 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
55 55 repo_group = user_util.create_repo_group()
56 56 repo_group_name = repo_group.group_name
57 57
58 58 description = 'description for newly created repo group'
59 59 form_data = rc_fixture._get_group_create_params(
60 60 group_name=repo_group.group_name,
61 61 group_description=description,
62 62 csrf_token=csrf_token,
63 63 repo_group_name=repo_group.group_name,
64 64 repo_group_owner=repo_group.user.username)
65 65
66 66 response = self.app.post(
67 67 route_path('edit_repo_group',
68 68 repo_group_name=repo_group.group_name),
69 69 form_data,
70 70 status=302)
71 71
72 72 assert_session_flash(
73 73 response, 'Repository Group `{}` updated successfully'.format(
74 74 repo_group_name))
75 75
76 76 def test_update_fails_when_parent_pointing_to_self(
77 77 self, csrf_token, user_util, autologin_user, rc_fixture):
78 78 group = user_util.create_repo_group()
79 79 response = self.app.post(
80 80 route_path('edit_repo_group', repo_group_name=group.group_name),
81 81 rc_fixture._get_group_create_params(
82 82 repo_group_name=group.group_name,
83 83 repo_group_owner=group.user.username,
84 84 repo_group=group.group_id,
85 85 csrf_token=csrf_token),
86 86 status=200
87 87 )
88 88 response.mustcontain(
89 89 '<span class="error-message">"{}" is not one of -1'.format(
90 90 group.group_id))
@@ -1,84 +1,84 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22 from rhodecode.model.db import Repository
23 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib.request, urllib.parse, urllib.error
27 27
28 28 base_url = {
29 29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 31 }[name].format(**kwargs)
32 32
33 33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 35 return base_url
36 36
37 37
38 38 @pytest.mark.backends("git", "hg")
39 39 @pytest.mark.usefixtures('autologin_user', 'app')
40 40 class TestPullRequestList(object):
41 41
42 42 @pytest.mark.parametrize('params, expected_title', [
43 43 ({'source': 0, 'closed': 1}, 'Closed'),
44 44 ({'source': 0, 'my': 1}, 'Created by me'),
45 45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 47 ({'source': 1}, 'From this repo'),
48 48 ])
49 49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 50 pull_request = pr_util.create_pull_request()
51 51
52 52 response = self.app.get(
53 53 route_path('pullrequest_show_all',
54 54 repo_name=pull_request.target_repo.repo_name,
55 55 params=params))
56 56
57 57 assert_response = response.assert_response()
58 58
59 59 element = assert_response.get_element('.title .active')
60 60 element_text = element.text_content()
61 61 assert expected_title == element_text
62 62
63 63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 64 pull_request = pr_util.create_pull_request()
65 65 response = self.app.get(
66 66 route_path('pullrequest_show_all_data',
67 67 repo_name=pull_request.target_repo.repo_name),
68 68 extra_environ=xhr_header)
69 69
70 70 assert response.json['recordsTotal'] == 1
71 71 assert response.json['data'][0]['description'] == 'Description'
72 72
73 73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 74 xss_description = "<script>alert('Hi!')</script>"
75 75 pull_request = pr_util.create_pull_request(description=xss_description)
76 76
77 77 response = self.app.get(
78 78 route_path('pullrequest_show_all_data',
79 79 repo_name=pull_request.target_repo.repo_name),
80 80 extra_environ=xhr_header)
81 81
82 82 assert response.json['recordsTotal'] == 1
83 83 assert response.json['data'][0]['description'] == \
84 84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,52 +1,52 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22 from rhodecode.model.db import Repository
23 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib.request, urllib.parse, urllib.error
27 27
28 28 base_url = {
29 29 'bookmarks_home': '/{repo_name}/bookmarks',
30 30 }[name].format(**kwargs)
31 31
32 32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 34 return base_url
35 35
36 36
37 37 @pytest.mark.usefixtures('autologin_user', 'app')
38 38 class TestBookmarks(object):
39 39
40 40 def test_index(self, backend):
41 41 if backend.alias == 'hg':
42 42 response = self.app.get(
43 43 route_path('bookmarks_home', repo_name=backend.repo_name))
44 44
45 45 repo = Repository.get_by_repo_name(backend.repo_name)
46 46 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
47 47 assert commit_id in response
48 48 assert obj_name in response
49 49 else:
50 50 self.app.get(
51 51 route_path('bookmarks_home', repo_name=backend.repo_name),
52 52 status=404)
@@ -1,48 +1,48 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22 from rhodecode.model.db import Repository
23 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib.request, urllib.parse, urllib.error
27 27
28 28 base_url = {
29 29 'branches_home': '/{repo_name}/branches',
30 30 }[name].format(**kwargs)
31 31
32 32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 34 return base_url
35 35
36 36
37 37 @pytest.mark.usefixtures('autologin_user', 'app')
38 38 class TestBranchesController(object):
39 39
40 40 def test_index(self, backend):
41 41 response = self.app.get(
42 42 route_path('branches_home', repo_name=backend.repo_name))
43 43
44 44 repo = Repository.get_by_repo_name(backend.repo_name)
45 45
46 46 for commit_id, obj_name in repo.scm_instance().branches.items():
47 47 assert commit_id in response
48 48 assert obj_name in response
@@ -1,220 +1,220 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import re
22 22
23 23 import pytest
24 24
25 25 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
26 26 from rhodecode.tests import TestController
27 27
28 28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 33
34 34 base_url = {
35 35 'repo_changelog': '/{repo_name}/changelog',
36 36 'repo_commits': '/{repo_name}/commits',
37 37 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
38 38 'repo_commits_elements': '/{repo_name}/commits_elements',
39 39 }[name].format(**kwargs)
40 40
41 41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 43 return base_url
44 44
45 45
46 46 def assert_commits_on_page(response, indexes):
47 47 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
48 48 assert found_indexes == indexes
49 49
50 50
51 51 class TestChangelogController(TestController):
52 52
53 53 def test_commits_page(self, backend):
54 54 self.log_user()
55 55 response = self.app.get(
56 56 route_path('repo_commits', repo_name=backend.repo_name))
57 57
58 58 first_idx = -1
59 59 last_idx = -DEFAULT_CHANGELOG_SIZE
60 60 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
61 61
62 62 def test_changelog(self, backend):
63 63 self.log_user()
64 64 response = self.app.get(
65 65 route_path('repo_changelog', repo_name=backend.repo_name))
66 66
67 67 first_idx = -1
68 68 last_idx = -DEFAULT_CHANGELOG_SIZE
69 69 self.assert_commit_range_on_page(
70 70 response, first_idx, last_idx, backend)
71 71
72 72 @pytest.mark.backends("hg", "git")
73 73 def test_changelog_filtered_by_branch(self, backend):
74 74 self.log_user()
75 75 self.app.get(
76 76 route_path('repo_changelog', repo_name=backend.repo_name,
77 77 params=dict(branch=backend.default_branch_name)),
78 78 status=200)
79 79
80 80 @pytest.mark.backends("hg", "git")
81 81 def test_commits_filtered_by_branch(self, backend):
82 82 self.log_user()
83 83 self.app.get(
84 84 route_path('repo_commits', repo_name=backend.repo_name,
85 85 params=dict(branch=backend.default_branch_name)),
86 86 status=200)
87 87
88 88 @pytest.mark.backends("svn")
89 89 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
90 90 repo = backend['svn-simple-layout']
91 91 response = self.app.get(
92 92 route_path('repo_changelog', repo_name=repo.repo_name,
93 93 params=dict(branch='trunk')),
94 94 status=200)
95 95
96 96 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
97 97
98 98 def test_commits_filtered_by_wrong_branch(self, backend):
99 99 self.log_user()
100 100 branch = 'wrong-branch-name'
101 101 response = self.app.get(
102 102 route_path('repo_commits', repo_name=backend.repo_name,
103 103 params=dict(branch=branch)),
104 104 status=302)
105 105 expected_url = '/{repo}/commits/{branch}'.format(
106 106 repo=backend.repo_name, branch=branch)
107 107 assert expected_url in response.location
108 108 response = response.follow()
109 109 expected_warning = 'Branch {} is not found.'.format(branch)
110 110 assert expected_warning in response.body
111 111
112 112 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
113 113 def test_changelog_filtered_by_branch_with_merges(
114 114 self, autologin_user, backend):
115 115
116 116 # Note: The changelog of branch "b" does not contain the commit "a1"
117 117 # although this is a parent of commit "b1". And branch "b" has commits
118 118 # which have a smaller index than commit "a1".
119 119 commits = [
120 120 {'message': 'a'},
121 121 {'message': 'b', 'branch': 'b'},
122 122 {'message': 'a1', 'parents': ['a']},
123 123 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
124 124 ]
125 125 backend.create_repo(commits)
126 126
127 127 self.app.get(
128 128 route_path('repo_changelog', repo_name=backend.repo_name,
129 129 params=dict(branch='b')),
130 130 status=200)
131 131
132 132 @pytest.mark.backends("hg")
133 133 def test_commits_closed_branches(self, autologin_user, backend):
134 134 repo = backend['closed_branch']
135 135 response = self.app.get(
136 136 route_path('repo_commits', repo_name=repo.repo_name,
137 137 params=dict(branch='experimental')),
138 138 status=200)
139 139
140 140 assert_commits_on_page(response, indexes=[3, 1])
141 141
142 142 def test_changelog_pagination(self, backend):
143 143 self.log_user()
144 144 # pagination, walk up to page 6
145 145 changelog_url = route_path(
146 146 'repo_commits', repo_name=backend.repo_name)
147 147
148 148 for page in range(1, 7):
149 149 response = self.app.get(changelog_url, {'page': page})
150 150
151 151 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
152 152 last_idx = -DEFAULT_CHANGELOG_SIZE * page
153 153 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
154 154
155 155 def assert_commit_range_on_page(
156 156 self, response, first_idx, last_idx, backend):
157 157 input_template = (
158 158 """<input class="commit-range" """
159 159 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
160 160 """data-short-id="%(short_id)s" id="%(raw_id)s" """
161 161 """name="%(raw_id)s" type="checkbox" value="1" />"""
162 162 )
163 163
164 164 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
165 165 repo = backend.repo
166 166
167 167 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
168 168 response.mustcontain(
169 169 input_template % {'raw_id': first_commit_on_page.raw_id,
170 170 'idx': first_commit_on_page.idx,
171 171 'short_id': first_commit_on_page.short_id})
172 172
173 173 response.mustcontain(commit_span_template % (
174 174 first_commit_on_page.idx, first_commit_on_page.short_id)
175 175 )
176 176
177 177 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
178 178 response.mustcontain(
179 179 input_template % {'raw_id': last_commit_on_page.raw_id,
180 180 'idx': last_commit_on_page.idx,
181 181 'short_id': last_commit_on_page.short_id})
182 182 response.mustcontain(commit_span_template % (
183 183 last_commit_on_page.idx, last_commit_on_page.short_id)
184 184 )
185 185
186 186 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
187 187 first_span_of_next_page = commit_span_template % (
188 188 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
189 189 assert first_span_of_next_page not in response
190 190
191 191 @pytest.mark.parametrize('test_path', [
192 192 'vcs/exceptions.py',
193 193 '/vcs/exceptions.py',
194 194 '//vcs/exceptions.py'
195 195 ])
196 196 def test_commits_with_filenode(self, backend, test_path):
197 197 self.log_user()
198 198 response = self.app.get(
199 199 route_path('repo_commits_file', repo_name=backend.repo_name,
200 200 commit_id='tip', f_path=test_path),
201 201 )
202 202
203 203 # history commits messages
204 204 response.mustcontain('Added exceptions module, this time for real')
205 205 response.mustcontain('Added not implemented hg backend test case')
206 206 response.mustcontain('Added BaseChangeset class')
207 207
208 208 def test_commits_with_filenode_that_is_dirnode(self, backend):
209 209 self.log_user()
210 210 self.app.get(
211 211 route_path('repo_commits_file', repo_name=backend.repo_name,
212 212 commit_id='tip', f_path='/tests'),
213 213 status=302)
214 214
215 215 def test_commits_with_filenode_not_existing(self, backend):
216 216 self.log_user()
217 217 self.app.get(
218 218 route_path('repo_commits_file', repo_name=backend.repo_name,
219 219 commit_id='tip', f_path='wrong_path'),
220 220 status=302)
@@ -1,494 +1,494 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests import TestController
24 24
25 25 from rhodecode.model.db import ChangesetComment, Notification
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.lib import helpers as h
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32 32
33 33 base_url = {
34 34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 39 }[name].format(**kwargs)
40 40
41 41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 43 return base_url
44 44
45 45
46 46 @pytest.mark.backends("git", "hg", "svn")
47 47 class TestRepoCommitCommentsView(TestController):
48 48
49 49 @pytest.fixture(autouse=True)
50 50 def prepare(self, request, baseapp):
51 51 for x in ChangesetComment.query().all():
52 52 Session().delete(x)
53 53 Session().commit()
54 54
55 55 for x in Notification.query().all():
56 56 Session().delete(x)
57 57 Session().commit()
58 58
59 59 request.addfinalizer(self.cleanup)
60 60
61 61 def cleanup(self):
62 62 for x in ChangesetComment.query().all():
63 63 Session().delete(x)
64 64 Session().commit()
65 65
66 66 for x in Notification.query().all():
67 67 Session().delete(x)
68 68 Session().commit()
69 69
70 70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 71 def test_create(self, comment_type, backend):
72 72 self.log_user()
73 73 commit = backend.repo.get_commit('300')
74 74 commit_id = commit.raw_id
75 75 text = u'CommentOnCommit'
76 76
77 77 params = {'text': text, 'csrf_token': self.csrf_token,
78 78 'comment_type': comment_type}
79 79 self.app.post(
80 80 route_path('repo_commit_comment_create',
81 81 repo_name=backend.repo_name, commit_id=commit_id),
82 82 params=params)
83 83
84 84 response = self.app.get(
85 85 route_path('repo_commit',
86 86 repo_name=backend.repo_name, commit_id=commit_id))
87 87
88 88 # test DB
89 89 assert ChangesetComment.query().count() == 1
90 90 assert_comment_links(response, ChangesetComment.query().count(), 0)
91 91
92 92 assert Notification.query().count() == 1
93 93 assert ChangesetComment.query().count() == 1
94 94
95 95 notification = Notification.query().all()[0]
96 96
97 97 comment_id = ChangesetComment.query().first().comment_id
98 98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99 99
100 100 author = notification.created_by_user.username_and_name
101 101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 102 author, comment_type, h.show_id(commit), backend.repo_name)
103 103 assert sbj == notification.subject
104 104
105 105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 106 backend.repo_name, commit_id, comment_id))
107 107 assert lnk in notification.body
108 108
109 109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 110 def test_create_inline(self, comment_type, backend):
111 111 self.log_user()
112 112 commit = backend.repo.get_commit('300')
113 113 commit_id = commit.raw_id
114 114 text = u'CommentOnCommit'
115 115 f_path = 'vcs/web/simplevcs/views/repository.py'
116 116 line = 'n1'
117 117
118 118 params = {'text': text, 'f_path': f_path, 'line': line,
119 119 'comment_type': comment_type,
120 120 'csrf_token': self.csrf_token}
121 121
122 122 self.app.post(
123 123 route_path('repo_commit_comment_create',
124 124 repo_name=backend.repo_name, commit_id=commit_id),
125 125 params=params)
126 126
127 127 response = self.app.get(
128 128 route_path('repo_commit',
129 129 repo_name=backend.repo_name, commit_id=commit_id))
130 130
131 131 # test DB
132 132 assert ChangesetComment.query().count() == 1
133 133 assert_comment_links(response, 0, ChangesetComment.query().count())
134 134
135 135 if backend.alias == 'svn':
136 136 response.mustcontain(
137 137 '''data-f-path="vcs/commands/summary.py" '''
138 138 '''data-anchor-id="c-300-ad05457a43f8"'''
139 139 )
140 140 if backend.alias == 'git':
141 141 response.mustcontain(
142 142 '''data-f-path="vcs/backends/hg.py" '''
143 143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 144 )
145 145
146 146 if backend.alias == 'hg':
147 147 response.mustcontain(
148 148 '''data-f-path="vcs/backends/hg.py" '''
149 149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 150 )
151 151
152 152 assert Notification.query().count() == 1
153 153 assert ChangesetComment.query().count() == 1
154 154
155 155 notification = Notification.query().all()[0]
156 156 comment = ChangesetComment.query().first()
157 157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158 158
159 159 assert comment.revision == commit_id
160 160
161 161 author = notification.created_by_user.username_and_name
162 162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164 164
165 165 assert sbj == notification.subject
166 166
167 167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 168 backend.repo_name, commit_id, comment.comment_id))
169 169 assert lnk in notification.body
170 170 assert 'on line n1' in notification.body
171 171
172 172 def test_create_with_mention(self, backend):
173 173 self.log_user()
174 174
175 175 commit_id = backend.repo.get_commit('300').raw_id
176 176 text = u'@test_regular check CommentOnCommit'
177 177
178 178 params = {'text': text, 'csrf_token': self.csrf_token}
179 179 self.app.post(
180 180 route_path('repo_commit_comment_create',
181 181 repo_name=backend.repo_name, commit_id=commit_id),
182 182 params=params)
183 183
184 184 response = self.app.get(
185 185 route_path('repo_commit',
186 186 repo_name=backend.repo_name, commit_id=commit_id))
187 187 # test DB
188 188 assert ChangesetComment.query().count() == 1
189 189 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 190
191 191 notification = Notification.query().one()
192 192
193 193 assert len(notification.recipients) == 2
194 194 users = [x.username for x in notification.recipients]
195 195
196 196 # test_regular gets notification by @mention
197 197 assert sorted(users) == [u'test_admin', u'test_regular']
198 198
199 199 def test_create_with_status_change(self, backend):
200 200 self.log_user()
201 201 commit = backend.repo.get_commit('300')
202 202 commit_id = commit.raw_id
203 203 text = u'CommentOnCommit'
204 204 f_path = 'vcs/web/simplevcs/views/repository.py'
205 205 line = 'n1'
206 206
207 207 params = {'text': text, 'changeset_status': 'approved',
208 208 'csrf_token': self.csrf_token}
209 209
210 210 self.app.post(
211 211 route_path(
212 212 'repo_commit_comment_create',
213 213 repo_name=backend.repo_name, commit_id=commit_id),
214 214 params=params)
215 215
216 216 response = self.app.get(
217 217 route_path('repo_commit',
218 218 repo_name=backend.repo_name, commit_id=commit_id))
219 219
220 220 # test DB
221 221 assert ChangesetComment.query().count() == 1
222 222 assert_comment_links(response, ChangesetComment.query().count(), 0)
223 223
224 224 assert Notification.query().count() == 1
225 225 assert ChangesetComment.query().count() == 1
226 226
227 227 notification = Notification.query().all()[0]
228 228
229 229 comment_id = ChangesetComment.query().first().comment_id
230 230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231 231
232 232 author = notification.created_by_user.username_and_name
233 233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 234 author, h.show_id(commit), backend.repo_name)
235 235 assert sbj == notification.subject
236 236
237 237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 238 backend.repo_name, commit_id, comment_id))
239 239 assert lnk in notification.body
240 240
241 241 def test_delete(self, backend):
242 242 self.log_user()
243 243 commit_id = backend.repo.get_commit('300').raw_id
244 244 text = u'CommentOnCommit'
245 245
246 246 params = {'text': text, 'csrf_token': self.csrf_token}
247 247 self.app.post(
248 248 route_path(
249 249 'repo_commit_comment_create',
250 250 repo_name=backend.repo_name, commit_id=commit_id),
251 251 params=params)
252 252
253 253 comments = ChangesetComment.query().all()
254 254 assert len(comments) == 1
255 255 comment_id = comments[0].comment_id
256 256
257 257 self.app.post(
258 258 route_path('repo_commit_comment_delete',
259 259 repo_name=backend.repo_name,
260 260 commit_id=commit_id,
261 261 comment_id=comment_id),
262 262 params={'csrf_token': self.csrf_token})
263 263
264 264 comments = ChangesetComment.query().all()
265 265 assert len(comments) == 0
266 266
267 267 response = self.app.get(
268 268 route_path('repo_commit',
269 269 repo_name=backend.repo_name, commit_id=commit_id))
270 270 assert_comment_links(response, 0, 0)
271 271
272 272 def test_edit(self, backend):
273 273 self.log_user()
274 274 commit_id = backend.repo.get_commit('300').raw_id
275 275 text = u'CommentOnCommit'
276 276
277 277 params = {'text': text, 'csrf_token': self.csrf_token}
278 278 self.app.post(
279 279 route_path(
280 280 'repo_commit_comment_create',
281 281 repo_name=backend.repo_name, commit_id=commit_id),
282 282 params=params)
283 283
284 284 comments = ChangesetComment.query().all()
285 285 assert len(comments) == 1
286 286 comment_id = comments[0].comment_id
287 287 test_text = 'test_text'
288 288 self.app.post(
289 289 route_path(
290 290 'repo_commit_comment_edit',
291 291 repo_name=backend.repo_name,
292 292 commit_id=commit_id,
293 293 comment_id=comment_id,
294 294 ),
295 295 params={
296 296 'csrf_token': self.csrf_token,
297 297 'text': test_text,
298 298 'version': '0',
299 299 })
300 300
301 301 text_form_db = ChangesetComment.query().filter(
302 302 ChangesetComment.comment_id == comment_id).first().text
303 303 assert test_text == text_form_db
304 304
305 305 def test_edit_without_change(self, backend):
306 306 self.log_user()
307 307 commit_id = backend.repo.get_commit('300').raw_id
308 308 text = u'CommentOnCommit'
309 309
310 310 params = {'text': text, 'csrf_token': self.csrf_token}
311 311 self.app.post(
312 312 route_path(
313 313 'repo_commit_comment_create',
314 314 repo_name=backend.repo_name, commit_id=commit_id),
315 315 params=params)
316 316
317 317 comments = ChangesetComment.query().all()
318 318 assert len(comments) == 1
319 319 comment_id = comments[0].comment_id
320 320
321 321 response = self.app.post(
322 322 route_path(
323 323 'repo_commit_comment_edit',
324 324 repo_name=backend.repo_name,
325 325 commit_id=commit_id,
326 326 comment_id=comment_id,
327 327 ),
328 328 params={
329 329 'csrf_token': self.csrf_token,
330 330 'text': text,
331 331 'version': '0',
332 332 },
333 333 status=404,
334 334 )
335 335 assert response.status_int == 404
336 336
337 337 def test_edit_try_edit_already_edited(self, backend):
338 338 self.log_user()
339 339 commit_id = backend.repo.get_commit('300').raw_id
340 340 text = u'CommentOnCommit'
341 341
342 342 params = {'text': text, 'csrf_token': self.csrf_token}
343 343 self.app.post(
344 344 route_path(
345 345 'repo_commit_comment_create',
346 346 repo_name=backend.repo_name, commit_id=commit_id
347 347 ),
348 348 params=params,
349 349 )
350 350
351 351 comments = ChangesetComment.query().all()
352 352 assert len(comments) == 1
353 353 comment_id = comments[0].comment_id
354 354 test_text = 'test_text'
355 355 self.app.post(
356 356 route_path(
357 357 'repo_commit_comment_edit',
358 358 repo_name=backend.repo_name,
359 359 commit_id=commit_id,
360 360 comment_id=comment_id,
361 361 ),
362 362 params={
363 363 'csrf_token': self.csrf_token,
364 364 'text': test_text,
365 365 'version': '0',
366 366 }
367 367 )
368 368 test_text_v2 = 'test_v2'
369 369 response = self.app.post(
370 370 route_path(
371 371 'repo_commit_comment_edit',
372 372 repo_name=backend.repo_name,
373 373 commit_id=commit_id,
374 374 comment_id=comment_id,
375 375 ),
376 376 params={
377 377 'csrf_token': self.csrf_token,
378 378 'text': test_text_v2,
379 379 'version': '0',
380 380 },
381 381 status=409,
382 382 )
383 383 assert response.status_int == 409
384 384
385 385 text_form_db = ChangesetComment.query().filter(
386 386 ChangesetComment.comment_id == comment_id).first().text
387 387
388 388 assert test_text == text_form_db
389 389 assert test_text_v2 != text_form_db
390 390
391 391 def test_edit_forbidden_for_immutable_comments(self, backend):
392 392 self.log_user()
393 393 commit_id = backend.repo.get_commit('300').raw_id
394 394 text = u'CommentOnCommit'
395 395
396 396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 397 self.app.post(
398 398 route_path(
399 399 'repo_commit_comment_create',
400 400 repo_name=backend.repo_name,
401 401 commit_id=commit_id,
402 402 ),
403 403 params=params
404 404 )
405 405
406 406 comments = ChangesetComment.query().all()
407 407 assert len(comments) == 1
408 408 comment_id = comments[0].comment_id
409 409
410 410 comment = ChangesetComment.get(comment_id)
411 411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 412 Session().add(comment)
413 413 Session().commit()
414 414
415 415 response = self.app.post(
416 416 route_path(
417 417 'repo_commit_comment_edit',
418 418 repo_name=backend.repo_name,
419 419 commit_id=commit_id,
420 420 comment_id=comment_id,
421 421 ),
422 422 params={
423 423 'csrf_token': self.csrf_token,
424 424 'text': 'test_text',
425 425 },
426 426 status=403,
427 427 )
428 428 assert response.status_int == 403
429 429
430 430 def test_delete_forbidden_for_immutable_comments(self, backend):
431 431 self.log_user()
432 432 commit_id = backend.repo.get_commit('300').raw_id
433 433 text = u'CommentOnCommit'
434 434
435 435 params = {'text': text, 'csrf_token': self.csrf_token}
436 436 self.app.post(
437 437 route_path(
438 438 'repo_commit_comment_create',
439 439 repo_name=backend.repo_name, commit_id=commit_id),
440 440 params=params)
441 441
442 442 comments = ChangesetComment.query().all()
443 443 assert len(comments) == 1
444 444 comment_id = comments[0].comment_id
445 445
446 446 comment = ChangesetComment.get(comment_id)
447 447 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 448 Session().add(comment)
449 449 Session().commit()
450 450
451 451 self.app.post(
452 452 route_path('repo_commit_comment_delete',
453 453 repo_name=backend.repo_name,
454 454 commit_id=commit_id,
455 455 comment_id=comment_id),
456 456 params={'csrf_token': self.csrf_token},
457 457 status=403)
458 458
459 459 @pytest.mark.parametrize('renderer, text_input, output', [
460 460 ('rst', 'plain text', '<p>plain text</p>'),
461 461 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 462 ('rst', '*italics*', '<em>italics</em>'),
463 463 ('rst', '**bold**', '<strong>bold</strong>'),
464 464 ('markdown', 'plain text', '<p>plain text</p>'),
465 465 ('markdown', '# header', '<h1>header</h1>'),
466 466 ('markdown', '*italics*', '<em>italics</em>'),
467 467 ('markdown', '**bold**', '<strong>bold</strong>'),
468 468 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 469 'md-header', 'md-italics', 'md-bold', ])
470 470 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 471 self.log_user()
472 472 params = {
473 473 'renderer': renderer,
474 474 'text': text_input,
475 475 'csrf_token': self.csrf_token
476 476 }
477 477 commit_id = '0' * 16 # fake this for tests
478 478 response = self.app.post(
479 479 route_path('repo_commit_comment_preview',
480 480 repo_name=backend.repo_name, commit_id=commit_id,),
481 481 params=params,
482 482 extra_environ=xhr_header)
483 483
484 484 response.mustcontain(output)
485 485
486 486
487 487 def assert_comment_links(response, comments, inline_comments):
488 488 response.mustcontain(
489 489 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 490 response.mustcontain(
491 491 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
492 492
493 493
494 494
@@ -1,327 +1,327 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
24 24 from rhodecode.lib.helpers import _shorten_commit_id
25 25
26 26
27 27 def route_path(name, params=None, **kwargs):
28 import urllib
28 import urllib.request, urllib.parse, urllib.error
29 29
30 30 base_url = {
31 31 'repo_commit': '/{repo_name}/changeset/{commit_id}',
32 32 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
33 33 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
34 34 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
35 35 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
36 36 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
37 37 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
38 38 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
39 39 }[name].format(**kwargs)
40 40
41 41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 43 return base_url
44 44
45 45
46 46 @pytest.mark.usefixtures("app")
47 47 class TestRepoCommitView(object):
48 48
49 49 def test_show_commit(self, backend):
50 50 commit_id = self.commit_id[backend.alias]
51 51 response = self.app.get(route_path(
52 52 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
53 53 response.mustcontain('Added a symlink')
54 54 response.mustcontain(commit_id)
55 55 response.mustcontain('No newline at end of file')
56 56
57 57 def test_show_raw(self, backend):
58 58 commit_id = self.commit_id[backend.alias]
59 59 response = self.app.get(route_path(
60 60 'repo_commit_raw',
61 61 repo_name=backend.repo_name, commit_id=commit_id))
62 62 assert response.body == self.diffs[backend.alias]
63 63
64 64 def test_show_raw_patch(self, backend):
65 65 response = self.app.get(route_path(
66 66 'repo_commit_patch', repo_name=backend.repo_name,
67 67 commit_id=self.commit_id[backend.alias]))
68 68 assert response.body == self.patches[backend.alias]
69 69
70 70 def test_commit_download(self, backend):
71 71 response = self.app.get(route_path(
72 72 'repo_commit_download',
73 73 repo_name=backend.repo_name,
74 74 commit_id=self.commit_id[backend.alias]))
75 75 assert response.body == self.diffs[backend.alias]
76 76
77 77 def test_single_commit_page_different_ops(self, backend):
78 78 commit_id = {
79 79 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
80 80 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
81 81 'svn': '337',
82 82 }
83 83 diff_stat = {
84 84 'hg': (21, 943, 288),
85 85 'git': (20, 941, 286),
86 86 'svn': (21, 943, 288),
87 87 }
88 88
89 89 commit_id = commit_id[backend.alias]
90 90 response = self.app.get(route_path(
91 91 'repo_commit',
92 92 repo_name=backend.repo_name, commit_id=commit_id))
93 93
94 94 response.mustcontain(_shorten_commit_id(commit_id))
95 95
96 96 compare_page = ComparePage(response)
97 97 file_changes = diff_stat[backend.alias]
98 98 compare_page.contains_change_summary(*file_changes)
99 99
100 100 # files op files
101 101 response.mustcontain('File not present at commit: %s' %
102 102 _shorten_commit_id(commit_id))
103 103
104 104 # svn uses a different filename
105 105 if backend.alias == 'svn':
106 106 response.mustcontain('new file 10644')
107 107 else:
108 108 response.mustcontain('new file 100644')
109 109 response.mustcontain('Changed theme to ADC theme') # commit msg
110 110
111 111 self._check_new_diff_menus(response, right_menu=True)
112 112
113 113 def test_commit_range_page_different_ops(self, backend):
114 114 commit_id_range = {
115 115 'hg': (
116 116 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
117 117 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
118 118 'git': (
119 119 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
120 120 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
121 121 'svn': (
122 122 '335',
123 123 '337'),
124 124 }
125 125 commit_ids = commit_id_range[backend.alias]
126 126 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
127 127 response = self.app.get(route_path(
128 128 'repo_commit',
129 129 repo_name=backend.repo_name, commit_id=commit_id))
130 130
131 131 response.mustcontain(_shorten_commit_id(commit_ids[0]))
132 132 response.mustcontain(_shorten_commit_id(commit_ids[1]))
133 133
134 134 compare_page = ComparePage(response)
135 135
136 136 # svn is special
137 137 if backend.alias == 'svn':
138 138 response.mustcontain('new file 10644')
139 139 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
140 140 compare_page.contains_change_summary(*file_changes)
141 141 elif backend.alias == 'git':
142 142 response.mustcontain('new file 100644')
143 143 for file_changes in [(12, 222, 20), (20, 941, 286)]:
144 144 compare_page.contains_change_summary(*file_changes)
145 145 else:
146 146 response.mustcontain('new file 100644')
147 147 for file_changes in [(12, 222, 20), (21, 943, 288)]:
148 148 compare_page.contains_change_summary(*file_changes)
149 149
150 150 # files op files
151 151 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
152 152 response.mustcontain('Added docstrings to vcs.cli') # commit msg
153 153 response.mustcontain('Changed theme to ADC theme') # commit msg
154 154
155 155 self._check_new_diff_menus(response)
156 156
157 157 def test_combined_compare_commit_page_different_ops(self, backend):
158 158 commit_id_range = {
159 159 'hg': (
160 160 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
161 161 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
162 162 'git': (
163 163 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
164 164 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
165 165 'svn': (
166 166 '335',
167 167 '337'),
168 168 }
169 169 commit_ids = commit_id_range[backend.alias]
170 170 response = self.app.get(route_path(
171 171 'repo_compare',
172 172 repo_name=backend.repo_name,
173 173 source_ref_type='rev', source_ref=commit_ids[0],
174 174 target_ref_type='rev', target_ref=commit_ids[1], ))
175 175
176 176 response.mustcontain(_shorten_commit_id(commit_ids[0]))
177 177 response.mustcontain(_shorten_commit_id(commit_ids[1]))
178 178
179 179 # files op files
180 180 response.mustcontain('File not present at commit: %s' %
181 181 _shorten_commit_id(commit_ids[1]))
182 182
183 183 compare_page = ComparePage(response)
184 184
185 185 # svn is special
186 186 if backend.alias == 'svn':
187 187 response.mustcontain('new file 10644')
188 188 file_changes = (32, 1179, 310)
189 189 compare_page.contains_change_summary(*file_changes)
190 190 elif backend.alias == 'git':
191 191 response.mustcontain('new file 100644')
192 192 file_changes = (31, 1163, 306)
193 193 compare_page.contains_change_summary(*file_changes)
194 194 else:
195 195 response.mustcontain('new file 100644')
196 196 file_changes = (32, 1165, 308)
197 197 compare_page.contains_change_summary(*file_changes)
198 198
199 199 response.mustcontain('Added docstrings to vcs.cli') # commit msg
200 200 response.mustcontain('Changed theme to ADC theme') # commit msg
201 201
202 202 self._check_new_diff_menus(response)
203 203
204 204 def test_changeset_range(self, backend):
205 205 self._check_changeset_range(
206 206 backend, self.commit_id_range, self.commit_id_range_result)
207 207
208 208 def test_changeset_range_with_initial_commit(self, backend):
209 209 commit_id_range = {
210 210 'hg': (
211 211 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
212 212 '...6cba7170863a2411822803fa77a0a264f1310b35'),
213 213 'git': (
214 214 'c1214f7e79e02fc37156ff215cd71275450cffc3'
215 215 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
216 216 'svn': '1...3',
217 217 }
218 218 commit_id_range_result = {
219 219 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
220 220 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
221 221 'svn': ['1', '2', '3'],
222 222 }
223 223 self._check_changeset_range(
224 224 backend, commit_id_range, commit_id_range_result)
225 225
226 226 def _check_changeset_range(
227 227 self, backend, commit_id_ranges, commit_id_range_result):
228 228 response = self.app.get(
229 229 route_path('repo_commit',
230 230 repo_name=backend.repo_name,
231 231 commit_id=commit_id_ranges[backend.alias]))
232 232
233 233 expected_result = commit_id_range_result[backend.alias]
234 234 response.mustcontain('{} commits'.format(len(expected_result)))
235 235 for commit_id in expected_result:
236 236 response.mustcontain(commit_id)
237 237
238 238 commit_id = {
239 239 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
240 240 'svn': '393',
241 241 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
242 242 }
243 243
244 244 commit_id_range = {
245 245 'hg': (
246 246 'a53d9201d4bc278910d416d94941b7ea007ecd52'
247 247 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
248 248 'git': (
249 249 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
250 250 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
251 251 'svn': '391...393',
252 252 }
253 253
254 254 commit_id_range_result = {
255 255 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
256 256 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
257 257 'svn': ['391', '392', '393'],
258 258 }
259 259
260 260 diffs = {
261 261 'hg': r"""diff --git a/README b/README
262 262 new file mode 120000
263 263 --- /dev/null
264 264 +++ b/README
265 265 @@ -0,0 +1,1 @@
266 266 +README.rst
267 267 \ No newline at end of file
268 268 """,
269 269 'git': r"""diff --git a/README b/README
270 270 new file mode 120000
271 271 index 0000000..92cacd2
272 272 --- /dev/null
273 273 +++ b/README
274 274 @@ -0,0 +1 @@
275 275 +README.rst
276 276 \ No newline at end of file
277 277 """,
278 278 'svn': """Index: README
279 279 ===================================================================
280 280 diff --git a/README b/README
281 281 new file mode 10644
282 282 --- /dev/null\t(revision 0)
283 283 +++ b/README\t(revision 393)
284 284 @@ -0,0 +1 @@
285 285 +link README.rst
286 286 \\ No newline at end of file
287 287 """,
288 288 }
289 289
290 290 patches = {
291 291 'hg': r"""# HG changeset patch
292 292 # User Marcin Kuzminski <marcin@python-works.com>
293 293 # Date 2014-01-07 12:21:40
294 294 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
295 295 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
296 296
297 297 Added a symlink
298 298
299 299 """ + diffs['hg'],
300 300 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
301 301 From: Marcin Kuzminski <marcin@python-works.com>
302 302 Date: 2014-01-07 12:22:20
303 303 Subject: [PATCH] Added a symlink
304 304
305 305 ---
306 306
307 307 """ + diffs['git'],
308 308 'svn': r"""# SVN changeset patch
309 309 # User marcin
310 310 # Date 2014-09-02 12:25:22.071142
311 311 # Revision 393
312 312
313 313 Added a symlink
314 314
315 315 """ + diffs['svn'],
316 316 }
317 317
318 318 def _check_new_diff_menus(self, response, right_menu=False,):
319 319 # individual file diff menus
320 320 for elem in ['Show file before', 'Show file after']:
321 321 response.mustcontain(elem)
322 322
323 323 # right pane diff menus
324 324 if right_menu:
325 325 for elem in ['Hide whitespace changes', 'Toggle wide diff',
326 326 'Show full context diff']:
327 327 response.mustcontain(elem)
@@ -1,672 +1,672 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import mock
22 22 import pytest
23 23 import lxml.html
24 24
25 25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 26 from rhodecode.tests import assert_session_flash
27 27 from rhodecode.tests.utils import AssertResponse, commit_change
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32 32
33 33 base_url = {
34 34 'repo_compare_select': '/{repo_name}/compare',
35 35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 36 }[name].format(**kwargs)
37 37
38 38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 40 return base_url
41 41
42 42
43 43 @pytest.mark.usefixtures("autologin_user", "app")
44 44 class TestCompareView(object):
45 45
46 46 def test_compare_index_is_reached_at_least_once(self, backend):
47 47 repo = backend.repo
48 48 self.app.get(
49 49 route_path('repo_compare_select', repo_name=repo.repo_name))
50 50
51 51 @pytest.mark.xfail_backends("svn", reason="Requires pull")
52 52 def test_compare_remote_with_different_commit_indexes(self, backend):
53 53 # Preparing the following repository structure:
54 54 #
55 55 # Origin repository has two commits:
56 56 #
57 57 # 0 1
58 58 # A -- D
59 59 #
60 60 # The fork of it has a few more commits and "D" has a commit index
61 61 # which does not exist in origin.
62 62 #
63 63 # 0 1 2 3 4
64 64 # A -- -- -- D -- E
65 65 # \- B -- C
66 66 #
67 67
68 68 fork = backend.create_repo()
69 69
70 70 # prepare fork
71 71 commit0 = commit_change(
72 72 fork.repo_name, filename='file1', content='A',
73 73 message='A', vcs_type=backend.alias, parent=None, newfile=True)
74 74
75 75 commit1 = commit_change(
76 76 fork.repo_name, filename='file1', content='B',
77 77 message='B, child of A', vcs_type=backend.alias, parent=commit0)
78 78
79 79 commit_change( # commit 2
80 80 fork.repo_name, filename='file1', content='C',
81 81 message='C, child of B', vcs_type=backend.alias, parent=commit1)
82 82
83 83 commit3 = commit_change(
84 84 fork.repo_name, filename='file1', content='D',
85 85 message='D, child of A', vcs_type=backend.alias, parent=commit0)
86 86
87 87 commit4 = commit_change(
88 88 fork.repo_name, filename='file1', content='E',
89 89 message='E, child of D', vcs_type=backend.alias, parent=commit3)
90 90
91 91 # prepare origin repository, taking just the history up to D
92 92 origin = backend.create_repo()
93 93
94 94 origin_repo = origin.scm_instance(cache=False)
95 95 origin_repo.config.clear_section('hooks')
96 96 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
97 97 origin_repo = origin.scm_instance(cache=False) # cache rebuild
98 98
99 99 # Verify test fixture setup
100 100 # This does not work for git
101 101 if backend.alias != 'git':
102 102 assert 5 == len(fork.scm_instance().commit_ids)
103 103 assert 2 == len(origin_repo.commit_ids)
104 104
105 105 # Comparing the revisions
106 106 response = self.app.get(
107 107 route_path('repo_compare',
108 108 repo_name=origin.repo_name,
109 109 source_ref_type="rev", source_ref=commit3.raw_id,
110 110 target_ref_type="rev", target_ref=commit4.raw_id,
111 111 params=dict(merge='1', target_repo=fork.repo_name)
112 112 ))
113 113
114 114 compare_page = ComparePage(response)
115 115 compare_page.contains_commits([commit4])
116 116
117 117 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
118 118 def test_compare_forks_on_branch_extra_commits(self, backend):
119 119 repo1 = backend.create_repo()
120 120
121 121 # commit something !
122 122 commit0 = commit_change(
123 123 repo1.repo_name, filename='file1', content='line1\n',
124 124 message='commit1', vcs_type=backend.alias, parent=None,
125 125 newfile=True)
126 126
127 127 # fork this repo
128 128 repo2 = backend.create_fork()
129 129
130 130 # add two extra commit into fork
131 131 commit1 = commit_change(
132 132 repo2.repo_name, filename='file1', content='line1\nline2\n',
133 133 message='commit2', vcs_type=backend.alias, parent=commit0)
134 134
135 135 commit2 = commit_change(
136 136 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
137 137 message='commit3', vcs_type=backend.alias, parent=commit1)
138 138
139 139 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
140 140 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
141 141
142 142 response = self.app.get(
143 143 route_path('repo_compare',
144 144 repo_name=repo1.repo_name,
145 145 source_ref_type="branch", source_ref=commit_id2,
146 146 target_ref_type="branch", target_ref=commit_id1,
147 147 params=dict(merge='1', target_repo=repo2.repo_name)
148 148 ))
149 149
150 150 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
151 151 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
152 152
153 153 compare_page = ComparePage(response)
154 154 compare_page.contains_change_summary(1, 2, 0)
155 155 compare_page.contains_commits([commit1, commit2])
156 156
157 157 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
158 158 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
159 159
160 160 # Swap is removed when comparing branches since it's a PR feature and
161 161 # it is then a preview mode
162 162 compare_page.swap_is_hidden()
163 163 compare_page.target_source_are_disabled()
164 164
165 165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
166 166 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
167 167 repo1 = backend.create_repo()
168 168
169 169 # commit something !
170 170 commit0 = commit_change(
171 171 repo1.repo_name, filename='file1', content='line1\n',
172 172 message='commit1', vcs_type=backend.alias, parent=None,
173 173 newfile=True)
174 174
175 175 # fork this repo
176 176 repo2 = backend.create_fork()
177 177
178 178 # now commit something to origin repo
179 179 commit_change(
180 180 repo1.repo_name, filename='file2', content='line1file2\n',
181 181 message='commit2', vcs_type=backend.alias, parent=commit0,
182 182 newfile=True)
183 183
184 184 # add two extra commit into fork
185 185 commit1 = commit_change(
186 186 repo2.repo_name, filename='file1', content='line1\nline2\n',
187 187 message='commit2', vcs_type=backend.alias, parent=commit0)
188 188
189 189 commit2 = commit_change(
190 190 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
191 191 message='commit3', vcs_type=backend.alias, parent=commit1)
192 192
193 193 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
194 194 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
195 195
196 196 response = self.app.get(
197 197 route_path('repo_compare',
198 198 repo_name=repo1.repo_name,
199 199 source_ref_type="branch", source_ref=commit_id2,
200 200 target_ref_type="branch", target_ref=commit_id1,
201 201 params=dict(merge='1', target_repo=repo2.repo_name),
202 202 ))
203 203
204 204 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
205 205 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
206 206
207 207 compare_page = ComparePage(response)
208 208 compare_page.contains_change_summary(1, 2, 0)
209 209 compare_page.contains_commits([commit1, commit2])
210 210 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
211 211 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
212 212
213 213 # Swap is removed when comparing branches since it's a PR feature and
214 214 # it is then a preview mode
215 215 compare_page.swap_is_hidden()
216 216 compare_page.target_source_are_disabled()
217 217
218 218 @pytest.mark.xfail_backends("svn")
219 219 # TODO(marcink): no svn support for compare two seperate repos
220 220 def test_compare_of_unrelated_forks(self, backend):
221 221 orig = backend.create_repo(number_of_commits=1)
222 222 fork = backend.create_repo(number_of_commits=1)
223 223
224 224 response = self.app.get(
225 225 route_path('repo_compare',
226 226 repo_name=orig.repo_name,
227 227 source_ref_type="rev", source_ref="tip",
228 228 target_ref_type="rev", target_ref="tip",
229 229 params=dict(merge='1', target_repo=fork.repo_name),
230 230 ),
231 231 status=302)
232 232 response = response.follow()
233 233 response.mustcontain("Repositories unrelated.")
234 234
235 235 @pytest.mark.xfail_backends("svn")
236 236 def test_compare_cherry_pick_commits_from_bottom(self, backend):
237 237
238 238 # repo1:
239 239 # commit0:
240 240 # commit1:
241 241 # repo1-fork- in which we will cherry pick bottom commits
242 242 # commit0:
243 243 # commit1:
244 244 # commit2: x
245 245 # commit3: x
246 246 # commit4: x
247 247 # commit5:
248 248 # make repo1, and commit1+commit2
249 249
250 250 repo1 = backend.create_repo()
251 251
252 252 # commit something !
253 253 commit0 = commit_change(
254 254 repo1.repo_name, filename='file1', content='line1\n',
255 255 message='commit1', vcs_type=backend.alias, parent=None,
256 256 newfile=True)
257 257 commit1 = commit_change(
258 258 repo1.repo_name, filename='file1', content='line1\nline2\n',
259 259 message='commit2', vcs_type=backend.alias, parent=commit0)
260 260
261 261 # fork this repo
262 262 repo2 = backend.create_fork()
263 263
264 264 # now make commit3-6
265 265 commit2 = commit_change(
266 266 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
267 267 message='commit3', vcs_type=backend.alias, parent=commit1)
268 268 commit3 = commit_change(
269 269 repo1.repo_name, filename='file1',
270 270 content='line1\nline2\nline3\nline4\n', message='commit4',
271 271 vcs_type=backend.alias, parent=commit2)
272 272 commit4 = commit_change(
273 273 repo1.repo_name, filename='file1',
274 274 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
275 275 vcs_type=backend.alias, parent=commit3)
276 276 commit_change( # commit 5
277 277 repo1.repo_name, filename='file1',
278 278 content='line1\nline2\nline3\nline4\nline5\nline6\n',
279 279 message='commit6', vcs_type=backend.alias, parent=commit4)
280 280
281 281 response = self.app.get(
282 282 route_path('repo_compare',
283 283 repo_name=repo2.repo_name,
284 284 # parent of commit2, in target repo2
285 285 source_ref_type="rev", source_ref=commit1.raw_id,
286 286 target_ref_type="rev", target_ref=commit4.raw_id,
287 287 params=dict(merge='1', target_repo=repo1.repo_name),
288 288 ))
289 289 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
290 290 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
291 291
292 292 # files
293 293 compare_page = ComparePage(response)
294 294 compare_page.contains_change_summary(1, 3, 0)
295 295 compare_page.contains_commits([commit2, commit3, commit4])
296 296 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
297 297 compare_page.contains_file_links_and_anchors([('file1', anchor),])
298 298
299 299 @pytest.mark.xfail_backends("svn")
300 300 def test_compare_cherry_pick_commits_from_top(self, backend):
301 301 # repo1:
302 302 # commit0:
303 303 # commit1:
304 304 # repo1-fork- in which we will cherry pick bottom commits
305 305 # commit0:
306 306 # commit1:
307 307 # commit2:
308 308 # commit3: x
309 309 # commit4: x
310 310 # commit5: x
311 311
312 312 # make repo1, and commit1+commit2
313 313 repo1 = backend.create_repo()
314 314
315 315 # commit something !
316 316 commit0 = commit_change(
317 317 repo1.repo_name, filename='file1', content='line1\n',
318 318 message='commit1', vcs_type=backend.alias, parent=None,
319 319 newfile=True)
320 320 commit1 = commit_change(
321 321 repo1.repo_name, filename='file1', content='line1\nline2\n',
322 322 message='commit2', vcs_type=backend.alias, parent=commit0)
323 323
324 324 # fork this repo
325 325 backend.create_fork()
326 326
327 327 # now make commit3-6
328 328 commit2 = commit_change(
329 329 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
330 330 message='commit3', vcs_type=backend.alias, parent=commit1)
331 331 commit3 = commit_change(
332 332 repo1.repo_name, filename='file1',
333 333 content='line1\nline2\nline3\nline4\n', message='commit4',
334 334 vcs_type=backend.alias, parent=commit2)
335 335 commit4 = commit_change(
336 336 repo1.repo_name, filename='file1',
337 337 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
338 338 vcs_type=backend.alias, parent=commit3)
339 339 commit5 = commit_change(
340 340 repo1.repo_name, filename='file1',
341 341 content='line1\nline2\nline3\nline4\nline5\nline6\n',
342 342 message='commit6', vcs_type=backend.alias, parent=commit4)
343 343
344 344 response = self.app.get(
345 345 route_path('repo_compare',
346 346 repo_name=repo1.repo_name,
347 347 # parent of commit3, not in source repo2
348 348 source_ref_type="rev", source_ref=commit2.raw_id,
349 349 target_ref_type="rev", target_ref=commit5.raw_id,
350 350 params=dict(merge='1'),))
351 351
352 352 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
353 353 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
354 354
355 355 compare_page = ComparePage(response)
356 356 compare_page.contains_change_summary(1, 3, 0)
357 357 compare_page.contains_commits([commit3, commit4, commit5])
358 358
359 359 # files
360 360 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
361 361 compare_page.contains_file_links_and_anchors([('file1', anchor),])
362 362
363 363 @pytest.mark.xfail_backends("svn")
364 364 def test_compare_remote_branches(self, backend):
365 365 repo1 = backend.repo
366 366 repo2 = backend.create_fork()
367 367
368 368 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
369 369 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
370 370 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
371 371 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
372 372
373 373 response = self.app.get(
374 374 route_path('repo_compare',
375 375 repo_name=repo1.repo_name,
376 376 source_ref_type="rev", source_ref=commit_id1,
377 377 target_ref_type="rev", target_ref=commit_id2,
378 378 params=dict(merge='1', target_repo=repo2.repo_name),
379 379 ))
380 380
381 381 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
382 382 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
383 383
384 384 compare_page = ComparePage(response)
385 385
386 386 # outgoing commits between those commits
387 387 compare_page.contains_commits(
388 388 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
389 389
390 390 # files
391 391 compare_page.contains_file_links_and_anchors([
392 392 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
393 393 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
394 394 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
395 395 ])
396 396
397 397 @pytest.mark.xfail_backends("svn")
398 398 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
399 399 repo1 = backend.create_repo()
400 400 r1_name = repo1.repo_name
401 401
402 402 commit0 = commit_change(
403 403 repo=r1_name, filename='file1',
404 404 content='line1', message='commit1', vcs_type=backend.alias,
405 405 newfile=True)
406 406 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
407 407
408 408 # fork the repo1
409 409 repo2 = backend.create_fork()
410 410 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
411 411
412 412 self.r2_id = repo2.repo_id
413 413 r2_name = repo2.repo_name
414 414
415 415 commit1 = commit_change(
416 416 repo=r2_name, filename='file1-fork',
417 417 content='file1-line1-from-fork', message='commit1-fork',
418 418 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
419 419 newfile=True)
420 420
421 421 commit2 = commit_change(
422 422 repo=r2_name, filename='file2-fork',
423 423 content='file2-line1-from-fork', message='commit2-fork',
424 424 vcs_type=backend.alias, parent=commit1,
425 425 newfile=True)
426 426
427 427 commit_change( # commit 3
428 428 repo=r2_name, filename='file3-fork',
429 429 content='file3-line1-from-fork', message='commit3-fork',
430 430 vcs_type=backend.alias, parent=commit2, newfile=True)
431 431
432 432 # compare !
433 433 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
434 434 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
435 435
436 436 response = self.app.get(
437 437 route_path('repo_compare',
438 438 repo_name=r2_name,
439 439 source_ref_type="branch", source_ref=commit_id1,
440 440 target_ref_type="branch", target_ref=commit_id2,
441 441 params=dict(merge='1', target_repo=r1_name),
442 442 ))
443 443
444 444 response.mustcontain('%s@%s' % (r2_name, commit_id1))
445 445 response.mustcontain('%s@%s' % (r1_name, commit_id2))
446 446 response.mustcontain('No files')
447 447 response.mustcontain('No commits in this compare')
448 448
449 449 commit0 = commit_change(
450 450 repo=r1_name, filename='file2',
451 451 content='line1-added-after-fork', message='commit2-parent',
452 452 vcs_type=backend.alias, parent=None, newfile=True)
453 453
454 454 # compare !
455 455 response = self.app.get(
456 456 route_path('repo_compare',
457 457 repo_name=r2_name,
458 458 source_ref_type="branch", source_ref=commit_id1,
459 459 target_ref_type="branch", target_ref=commit_id2,
460 460 params=dict(merge='1', target_repo=r1_name),
461 461 ))
462 462
463 463 response.mustcontain('%s@%s' % (r2_name, commit_id1))
464 464 response.mustcontain('%s@%s' % (r1_name, commit_id2))
465 465
466 466 response.mustcontain("""commit2-parent""")
467 467 response.mustcontain("""line1-added-after-fork""")
468 468 compare_page = ComparePage(response)
469 469 compare_page.contains_change_summary(1, 1, 0)
470 470
471 471 @pytest.mark.xfail_backends("svn")
472 472 def test_compare_commits(self, backend, xhr_header):
473 473 commit0 = backend.repo.get_commit(commit_idx=0)
474 474 commit1 = backend.repo.get_commit(commit_idx=1)
475 475
476 476 response = self.app.get(
477 477 route_path('repo_compare',
478 478 repo_name=backend.repo_name,
479 479 source_ref_type="rev", source_ref=commit0.raw_id,
480 480 target_ref_type="rev", target_ref=commit1.raw_id,
481 481 params=dict(merge='1')
482 482 ),
483 483 extra_environ=xhr_header, )
484 484
485 485 # outgoing commits between those commits
486 486 compare_page = ComparePage(response)
487 487 compare_page.contains_commits(commits=[commit1])
488 488
489 489 def test_errors_when_comparing_unknown_source_repo(self, backend):
490 490 repo = backend.repo
491 491 badrepo = 'badrepo'
492 492
493 493 response = self.app.get(
494 494 route_path('repo_compare',
495 495 repo_name=badrepo,
496 496 source_ref_type="rev", source_ref='tip',
497 497 target_ref_type="rev", target_ref='tip',
498 498 params=dict(merge='1', target_repo=repo.repo_name)
499 499 ),
500 500 status=404)
501 501
502 502 def test_errors_when_comparing_unknown_target_repo(self, backend):
503 503 repo = backend.repo
504 504 badrepo = 'badrepo'
505 505
506 506 response = self.app.get(
507 507 route_path('repo_compare',
508 508 repo_name=repo.repo_name,
509 509 source_ref_type="rev", source_ref='tip',
510 510 target_ref_type="rev", target_ref='tip',
511 511 params=dict(merge='1', target_repo=badrepo),
512 512 ),
513 513 status=302)
514 514 redirected = response.follow()
515 515 redirected.mustcontain(
516 516 'Could not find the target repo: `{}`'.format(badrepo))
517 517
518 518 def test_compare_not_in_preview_mode(self, backend_stub):
519 519 commit0 = backend_stub.repo.get_commit(commit_idx=0)
520 520 commit1 = backend_stub.repo.get_commit(commit_idx=1)
521 521
522 522 response = self.app.get(
523 523 route_path('repo_compare',
524 524 repo_name=backend_stub.repo_name,
525 525 source_ref_type="rev", source_ref=commit0.raw_id,
526 526 target_ref_type="rev", target_ref=commit1.raw_id,
527 527 ))
528 528
529 529 # outgoing commits between those commits
530 530 compare_page = ComparePage(response)
531 531 compare_page.swap_is_visible()
532 532 compare_page.target_source_are_enabled()
533 533
534 534 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
535 535 orig = backend_hg.create_repo(number_of_commits=1)
536 536 fork = backend_hg.create_fork()
537 537
538 538 settings_util.create_repo_rhodecode_ui(
539 539 orig, 'extensions', value='', key='largefiles', active=False)
540 540 settings_util.create_repo_rhodecode_ui(
541 541 fork, 'extensions', value='', key='largefiles', active=True)
542 542
543 543 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
544 544 'MercurialRepository.compare')
545 545 with mock.patch(compare_module) as compare_mock:
546 546 compare_mock.side_effect = RepositoryRequirementError()
547 547
548 548 response = self.app.get(
549 549 route_path('repo_compare',
550 550 repo_name=orig.repo_name,
551 551 source_ref_type="rev", source_ref="tip",
552 552 target_ref_type="rev", target_ref="tip",
553 553 params=dict(merge='1', target_repo=fork.repo_name),
554 554 ),
555 555 status=302)
556 556
557 557 assert_session_flash(
558 558 response,
559 559 'Could not compare repos with different large file settings')
560 560
561 561
562 562 @pytest.mark.usefixtures("autologin_user")
563 563 class TestCompareControllerSvn(object):
564 564
565 565 def test_supports_references_with_path(self, app, backend_svn):
566 566 repo = backend_svn['svn-simple-layout']
567 567 commit_id = repo.get_commit(commit_idx=-1).raw_id
568 568 response = app.get(
569 569 route_path('repo_compare',
570 570 repo_name=repo.repo_name,
571 571 source_ref_type="tag",
572 572 source_ref="%s@%s" % ('tags/v0.1', commit_id),
573 573 target_ref_type="tag",
574 574 target_ref="%s@%s" % ('tags/v0.2', commit_id),
575 575 params=dict(merge='1'),
576 576 ),
577 577 status=200)
578 578
579 579 # Expecting no commits, since both paths are at the same revision
580 580 response.mustcontain('No commits in this compare')
581 581
582 582 # Should find only one file changed when comparing those two tags
583 583 response.mustcontain('example.py')
584 584 compare_page = ComparePage(response)
585 585 compare_page.contains_change_summary(1, 5, 1)
586 586
587 587 def test_shows_commits_if_different_ids(self, app, backend_svn):
588 588 repo = backend_svn['svn-simple-layout']
589 589 source_id = repo.get_commit(commit_idx=-6).raw_id
590 590 target_id = repo.get_commit(commit_idx=-1).raw_id
591 591 response = app.get(
592 592 route_path('repo_compare',
593 593 repo_name=repo.repo_name,
594 594 source_ref_type="tag",
595 595 source_ref="%s@%s" % ('tags/v0.1', source_id),
596 596 target_ref_type="tag",
597 597 target_ref="%s@%s" % ('tags/v0.2', target_id),
598 598 params=dict(merge='1')
599 599 ),
600 600 status=200)
601 601
602 602 # It should show commits
603 603 assert 'No commits in this compare' not in response.body
604 604
605 605 # Should find only one file changed when comparing those two tags
606 606 response.mustcontain('example.py')
607 607 compare_page = ComparePage(response)
608 608 compare_page.contains_change_summary(1, 5, 1)
609 609
610 610
611 611 class ComparePage(AssertResponse):
612 612 """
613 613 Abstracts the page template from the tests
614 614 """
615 615
616 616 def contains_file_links_and_anchors(self, files):
617 617 doc = lxml.html.fromstring(self.response.body)
618 618 for filename, file_id in files:
619 619 self.contains_one_anchor(file_id)
620 620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
621 621 assert len(diffblock) == 2
622 622 for lnk in diffblock[0].cssselect('a'):
623 623 if 'permalink' in lnk.text:
624 624 assert '#{}'.format(file_id) in lnk.attrib['href']
625 625 break
626 626 else:
627 627 pytest.fail('Unable to find permalink')
628 628
629 629 def contains_change_summary(self, files_changed, inserted, deleted):
630 630 template = (
631 631 '{files_changed} file{plural} changed: '
632 632 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
633 633 self.response.mustcontain(template.format(
634 634 files_changed=files_changed,
635 635 plural="s" if files_changed > 1 else "",
636 636 inserted=inserted,
637 637 deleted=deleted))
638 638
639 639 def contains_commits(self, commits, ancestors=None):
640 640 response = self.response
641 641
642 642 for commit in commits:
643 643 # Expecting to see the commit message in an element which
644 644 # has the ID "c-{commit.raw_id}"
645 645 self.element_contains('#c-' + commit.raw_id, commit.message)
646 646 self.contains_one_link(
647 647 'r%s:%s' % (commit.idx, commit.short_id),
648 648 self._commit_url(commit))
649 649
650 650 if ancestors:
651 651 response.mustcontain('Ancestor')
652 652 for ancestor in ancestors:
653 653 self.contains_one_link(
654 654 ancestor.short_id, self._commit_url(ancestor))
655 655
656 656 def _commit_url(self, commit):
657 657 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
658 658
659 659 def swap_is_hidden(self):
660 660 assert '<a id="btn-swap"' not in self.response.text
661 661
662 662 def swap_is_visible(self):
663 663 assert '<a id="btn-swap"' in self.response.text
664 664
665 665 def target_source_are_disabled(self):
666 666 response = self.response
667 667 response.mustcontain("var enable_fields = false;")
668 668 response.mustcontain('.select2("enable", enable_fields)')
669 669
670 670 def target_source_are_enabled(self):
671 671 response = self.response
672 672 response.mustcontain("var enable_fields = true;")
@@ -1,167 +1,167 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from .test_repo_compare import ComparePage
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28 28
29 29 base_url = {
30 30 'repo_compare_select': '/{repo_name}/compare',
31 31 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
32 32 }[name].format(**kwargs)
33 33
34 34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 36 return base_url
37 37
38 38
39 39 @pytest.mark.usefixtures("autologin_user", "app")
40 40 class TestCompareView(object):
41 41
42 42 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
43 43 def test_compare_tag(self, backend):
44 44 tag1 = 'v0.1.2'
45 45 tag2 = 'v0.1.3'
46 46 response = self.app.get(
47 47 route_path(
48 48 'repo_compare',
49 49 repo_name=backend.repo_name,
50 50 source_ref_type="tag", source_ref=tag1,
51 51 target_ref_type="tag", target_ref=tag2),
52 52 status=200)
53 53
54 54 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
55 55 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
56 56
57 57 # outgoing commits between tags
58 58 commit_indexes = {
59 59 'git': [113] + range(115, 121),
60 60 'hg': [112] + range(115, 121),
61 61 }
62 62 repo = backend.repo
63 63 commits = (repo.get_commit(commit_idx=idx)
64 64 for idx in commit_indexes[backend.alias])
65 65 compare_page = ComparePage(response)
66 66 compare_page.contains_change_summary(11, 94, 64)
67 67 compare_page.contains_commits(commits)
68 68
69 69 # files diff
70 70 short_id = short_id_new = ''
71 71 if backend.alias == 'git':
72 72 short_id = '5a3a8fb00555'
73 73 short_id_new = '0ba5f8a46600'
74 74 if backend.alias == 'hg':
75 75 short_id = '17544fbfcd33'
76 76 short_id_new = 'a7e60bff65d5'
77 77
78 78 compare_page.contains_file_links_and_anchors([
79 79 # modified
80 80 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
81 81 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
82 82 # added
83 83 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
84 84 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
85 85 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
86 86 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
87 87 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
88 88 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
89 89 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
90 90 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
91 91 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
92 92 ])
93 93
94 94 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
95 95 def test_compare_tag_branch(self, backend):
96 96 revisions = {
97 97 'hg': {
98 98 'tag': 'v0.2.0',
99 99 'branch': 'default',
100 100 'response': (147, 5701, 10177)
101 101 },
102 102 'git': {
103 103 'tag': 'v0.2.2',
104 104 'branch': 'master',
105 105 'response': (70, 1855, 3002)
106 106 },
107 107 }
108 108
109 109 # Backend specific data, depends on the test repository for
110 110 # functional tests.
111 111 data = revisions[backend.alias]
112 112
113 113 response = self.app.get(
114 114 route_path(
115 115 'repo_compare',
116 116 repo_name=backend.repo_name,
117 117 source_ref_type='branch', source_ref=data['branch'],
118 118 target_ref_type="tag", target_ref=data['tag'],
119 119 ))
120 120
121 121 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
122 122 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
123 123 compare_page = ComparePage(response)
124 124 compare_page.contains_change_summary(*data['response'])
125 125
126 126 def test_index_branch(self, backend):
127 127 head_id = backend.default_head_id
128 128 response = self.app.get(
129 129 route_path(
130 130 'repo_compare',
131 131 repo_name=backend.repo_name,
132 132 source_ref_type="branch", source_ref=head_id,
133 133 target_ref_type="branch", target_ref=head_id,
134 134 ))
135 135
136 136 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
137 137
138 138 # branches are equal
139 139 response.mustcontain('No files')
140 140 response.mustcontain('No commits in this compare')
141 141
142 142 def test_compare_commits(self, backend):
143 143 repo = backend.repo
144 144 commit1 = repo.get_commit(commit_idx=0)
145 145 commit1_short_id = commit1.short_id
146 146 commit2 = repo.get_commit(commit_idx=1)
147 147 commit2_short_id = commit2.short_id
148 148
149 149 response = self.app.get(
150 150 route_path(
151 151 'repo_compare',
152 152 repo_name=backend.repo_name,
153 153 source_ref_type="rev", source_ref=commit1.raw_id,
154 154 target_ref_type="rev", target_ref=commit2.raw_id,
155 155 ))
156 156 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
157 157 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
158 158 compare_page = ComparePage(response)
159 159
160 160 # files
161 161 compare_page.contains_change_summary(1, 7, 0)
162 162
163 163 # outgoing commits between those commits
164 164 compare_page.contains_commits([commit2])
165 165 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
166 166 response.mustcontain(anchor)
167 167 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,291 +1,291 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
24 24 from rhodecode.lib.vcs import nodes
25 25 from rhodecode.lib.vcs.backends.base import EmptyCommit
26 26 from rhodecode.tests.fixture import Fixture
27 27 from rhodecode.tests.utils import commit_change
28 28
29 29 fixture = Fixture()
30 30
31 31
32 32 def route_path(name, params=None, **kwargs):
33 import urllib
33 import urllib.request, urllib.parse, urllib.error
34 34
35 35 base_url = {
36 36 'repo_compare_select': '/{repo_name}/compare',
37 37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 38 }[name].format(**kwargs)
39 39
40 40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 42 return base_url
43 43
44 44
45 45 @pytest.mark.usefixtures("autologin_user", "app")
46 46 class TestSideBySideDiff(object):
47 47
48 48 def test_diff_sidebyside_single_commit(self, app, backend):
49 49 commit_id_range = {
50 50 'hg': {
51 51 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
52 52 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
53 53 'changes': (21, 943, 288),
54 54 },
55 55 'git': {
56 56 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
57 57 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
58 58 'changes': (20, 941, 286),
59 59 },
60 60
61 61 'svn': {
62 62 'commits': ['336',
63 63 '337'],
64 64 'changes': (21, 943, 288),
65 65 },
66 66 }
67 67
68 68 commit_info = commit_id_range[backend.alias]
69 69 commit2, commit1 = commit_info['commits']
70 70 file_changes = commit_info['changes']
71 71
72 72 response = self.app.get(route_path(
73 73 'repo_compare',
74 74 repo_name=backend.repo_name,
75 75 source_ref_type='rev',
76 76 source_ref=commit2,
77 77 target_repo=backend.repo_name,
78 78 target_ref_type='rev',
79 79 target_ref=commit1,
80 80 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
81 81 ))
82 82
83 83 compare_page = ComparePage(response)
84 84 compare_page.contains_change_summary(*file_changes)
85 85 response.mustcontain('Collapse 1 commit')
86 86
87 87 def test_diff_sidebyside_two_commits(self, app, backend):
88 88 commit_id_range = {
89 89 'hg': {
90 90 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
91 91 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
92 92 'changes': (32, 1165, 308),
93 93 },
94 94 'git': {
95 95 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
96 96 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
97 97 'changes': (31, 1163, 306),
98 98 },
99 99
100 100 'svn': {
101 101 'commits': ['335',
102 102 '337'],
103 103 'changes': (32, 1179, 310),
104 104 },
105 105 }
106 106
107 107 commit_info = commit_id_range[backend.alias]
108 108 commit2, commit1 = commit_info['commits']
109 109 file_changes = commit_info['changes']
110 110
111 111 response = self.app.get(route_path(
112 112 'repo_compare',
113 113 repo_name=backend.repo_name,
114 114 source_ref_type='rev',
115 115 source_ref=commit2,
116 116 target_repo=backend.repo_name,
117 117 target_ref_type='rev',
118 118 target_ref=commit1,
119 119 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
120 120 ))
121 121
122 122 compare_page = ComparePage(response)
123 123 compare_page.contains_change_summary(*file_changes)
124 124
125 125 response.mustcontain('Collapse 2 commits')
126 126
127 127 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
128 128 commit_id_range = {
129 129
130 130 'svn': {
131 131 'commits': ['330',
132 132 '337'],
133 133
134 134 },
135 135 }
136 136
137 137 commit_info = commit_id_range['svn']
138 138 commit2, commit1 = commit_info['commits']
139 139
140 140 response = self.app.get(route_path(
141 141 'repo_compare',
142 142 repo_name=backend_svn.repo_name,
143 143 source_ref_type='rev',
144 144 source_ref=commit2,
145 145 target_repo=backend_svn.repo_name,
146 146 target_ref_type='rev',
147 147 target_ref=commit1,
148 148 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
149 149 ))
150 150
151 151 response.mustcontain('Expand 7 commits')
152 152
153 153 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
154 154 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
155 155 f_path = 'test_sidebyside_file.py'
156 156 commit1_content = 'content-25d7e49c18b159446c\n'
157 157 commit2_content = 'content-603d6c72c46d953420\n'
158 158 repo = backend.create_repo()
159 159
160 160 commit1 = commit_change(
161 161 repo.repo_name, filename=f_path, content=commit1_content,
162 162 message='A', vcs_type=backend.alias, parent=None, newfile=True)
163 163
164 164 commit2 = commit_change(
165 165 repo.repo_name, filename=f_path, content=commit2_content,
166 166 message='B, child of A', vcs_type=backend.alias, parent=commit1)
167 167
168 168 response = self.app.get(route_path(
169 169 'repo_compare',
170 170 repo_name=repo.repo_name,
171 171 source_ref_type='rev',
172 172 source_ref=EmptyCommit().raw_id,
173 173 target_ref_type='rev',
174 174 target_ref=commit2.raw_id,
175 175 params=dict(diffmode='sidebyside')
176 176 ))
177 177
178 178 response.mustcontain('Collapse 2 commits')
179 179 response.mustcontain('123 file changed')
180 180
181 181 response.mustcontain(
182 182 'r%s:%s...r%s:%s' % (
183 183 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
184 184
185 185 response.mustcontain(f_path)
186 186
187 187 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
188 188 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
189 189 f_path = 'test_sidebyside_file.py'
190 190 commit1_content = 'content-25d7e49c18b159446c\n'
191 191 commit2_content = 'content-603d6c72c46d953420\n'
192 192 repo = backend.create_repo()
193 193
194 194 commit1 = commit_change(
195 195 repo.repo_name, filename=f_path, content=commit1_content,
196 196 message='A', vcs_type=backend.alias, parent=None, newfile=True)
197 197
198 198 commit2 = commit_change(
199 199 repo.repo_name, filename=f_path, content=commit2_content,
200 200 message='B, child of A', vcs_type=backend.alias, parent=commit1)
201 201
202 202 response = self.app.get(route_path(
203 203 'repo_compare',
204 204 repo_name=repo.repo_name,
205 205 source_ref_type='rev',
206 206 source_ref=EmptyCommit().raw_id,
207 207 target_ref_type='rev',
208 208 target_ref=commit2.raw_id,
209 209 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
210 210 ))
211 211
212 212 response.mustcontain('Collapse 2 commits')
213 213 response.mustcontain('1 file changed')
214 214
215 215 response.mustcontain(
216 216 'r%s:%s...r%s:%s' % (
217 217 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
218 218
219 219 response.mustcontain(f_path)
220 220
221 221 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
222 222 commits = [
223 223 {'message': 'First commit'},
224 224 {'message': 'Second commit'},
225 225 {'message': 'Commit with binary',
226 226 'added': [nodes.FileNode('file.empty', content='')]},
227 227 ]
228 228 f_path = 'file.empty'
229 229 repo = backend.create_repo(commits=commits)
230 230 commit1 = repo.get_commit(commit_idx=0)
231 231 commit2 = repo.get_commit(commit_idx=1)
232 232 commit3 = repo.get_commit(commit_idx=2)
233 233
234 234 response = self.app.get(route_path(
235 235 'repo_compare',
236 236 repo_name=repo.repo_name,
237 237 source_ref_type='rev',
238 238 source_ref=commit1.raw_id,
239 239 target_ref_type='rev',
240 240 target_ref=commit3.raw_id,
241 241 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
242 242 ))
243 243
244 244 response.mustcontain('Collapse 2 commits')
245 245 response.mustcontain('1 file changed')
246 246
247 247 response.mustcontain(
248 248 'r%s:%s...r%s:%s' % (
249 249 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
250 250
251 251 response.mustcontain(f_path)
252 252
253 253 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
254 254 commit_id_range = {
255 255 'hg': {
256 256 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
257 257 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
258 258 'changes': (1, 3, 3)
259 259 },
260 260 'git': {
261 261 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
262 262 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
263 263 'changes': (1, 3, 3)
264 264 },
265 265
266 266 'svn': {
267 267 'commits': ['335',
268 268 '337'],
269 269 'changes': (1, 3, 3)
270 270 },
271 271 }
272 272 f_path = 'docs/conf.py'
273 273
274 274 commit_info = commit_id_range[backend.alias]
275 275 commit2, commit1 = commit_info['commits']
276 276 file_changes = commit_info['changes']
277 277
278 278 response = self.app.get(route_path(
279 279 'repo_compare',
280 280 repo_name=backend.repo_name,
281 281 source_ref_type='rev',
282 282 source_ref=commit2,
283 283 target_ref_type='rev',
284 284 target_ref=commit1,
285 285 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
286 286 ))
287 287
288 288 response.mustcontain('Collapse 2 commits')
289 289
290 290 compare_page = ComparePage(response)
291 291 compare_page.contains_change_summary(*file_changes)
@@ -1,137 +1,137 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22 from rhodecode.model.auth_token import AuthTokenModel
23 23 from rhodecode.tests import TestController
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28 28
29 29 base_url = {
30 30 'rss_feed_home': '/{repo_name}/feed-rss',
31 31 'atom_feed_home': '/{repo_name}/feed-atom',
32 32 'rss_feed_home_old': '/{repo_name}/feed/rss',
33 33 'atom_feed_home_old': '/{repo_name}/feed/atom',
34 34 }[name].format(**kwargs)
35 35
36 36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 38 return base_url
39 39
40 40
41 41 class TestFeedView(TestController):
42 42
43 43 @pytest.mark.parametrize("feed_type,response_types,content_type",[
44 44 ('rss', ['<rss version="2.0"'],
45 45 "application/rss+xml"),
46 46 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
47 47 "application/atom+xml"),
48 48 ])
49 49 def test_feed(self, backend, feed_type, response_types, content_type):
50 50 self.log_user()
51 51 response = self.app.get(
52 52 route_path('{}_feed_home'.format(feed_type),
53 53 repo_name=backend.repo_name))
54 54
55 55 for content in response_types:
56 56 response.mustcontain(content)
57 57
58 58 assert response.content_type == content_type
59 59
60 60 @pytest.mark.parametrize("feed_type, content_type", [
61 61 ('rss', "application/rss+xml"),
62 62 ('atom', "application/atom+xml")
63 63 ])
64 64 def test_feed_with_auth_token(
65 65 self, backend, user_admin, feed_type, content_type):
66 66 auth_token = user_admin.feed_token
67 67 assert auth_token != ''
68 68
69 69 response = self.app.get(
70 70 route_path(
71 71 '{}_feed_home'.format(feed_type),
72 72 repo_name=backend.repo_name,
73 73 params=dict(auth_token=auth_token)),
74 74 status=200)
75 75
76 76 assert response.content_type == content_type
77 77
78 78 @pytest.mark.parametrize("feed_type, content_type", [
79 79 ('rss', "application/rss+xml"),
80 80 ('atom', "application/atom+xml")
81 81 ])
82 82 def test_feed_with_auth_token_by_uid(
83 83 self, backend, user_admin, feed_type, content_type):
84 84 auth_token = user_admin.feed_token
85 85 assert auth_token != ''
86 86
87 87 response = self.app.get(
88 88 route_path(
89 89 '{}_feed_home'.format(feed_type),
90 90 repo_name='_{}'.format(backend.repo.repo_id),
91 91 params=dict(auth_token=auth_token)),
92 92 status=200)
93 93
94 94 assert response.content_type == content_type
95 95
96 96 @pytest.mark.parametrize("feed_type, content_type", [
97 97 ('rss', "application/rss+xml"),
98 98 ('atom', "application/atom+xml")
99 99 ])
100 100 def test_feed_old_urls_with_auth_token(
101 101 self, backend, user_admin, feed_type, content_type):
102 102 auth_token = user_admin.feed_token
103 103 assert auth_token != ''
104 104
105 105 response = self.app.get(
106 106 route_path(
107 107 '{}_feed_home_old'.format(feed_type),
108 108 repo_name=backend.repo_name,
109 109 params=dict(auth_token=auth_token)),
110 110 status=200)
111 111
112 112 assert response.content_type == content_type
113 113
114 114 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
115 115 def test_feed_with_auth_token_of_wrong_type(
116 116 self, backend, user_util, feed_type):
117 117 user = user_util.create_user()
118 118 auth_token = AuthTokenModel().create(
119 119 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
120 120 auth_token = auth_token.api_key
121 121
122 122 self.app.get(
123 123 route_path(
124 124 '{}_feed_home'.format(feed_type),
125 125 repo_name=backend.repo_name,
126 126 params=dict(auth_token=auth_token)),
127 127 status=302)
128 128
129 129 auth_token = AuthTokenModel().create(
130 130 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
131 131 auth_token = auth_token.api_key
132 132 self.app.get(
133 133 route_path(
134 134 '{}_feed_home'.format(feed_type),
135 135 repo_name=backend.repo_name,
136 136 params=dict(auth_token=auth_token)),
137 137 status=200)
@@ -1,1092 +1,1092 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import os
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.compat import OrderedDict
30 30 from rhodecode.lib.ext_json import json
31 31 from rhodecode.lib.vcs import nodes
32 32
33 33 from rhodecode.lib.vcs.conf import settings
34 34 from rhodecode.tests import assert_session_flash
35 35 from rhodecode.tests.fixture import Fixture
36 36 from rhodecode.model.db import Session
37 37
38 38 fixture = Fixture()
39 39
40 40
41 41 def get_node_history(backend_type):
42 42 return {
43 43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 46 }[backend_type]
47 47
48 48
49 49 def route_path(name, params=None, **kwargs):
50 import urllib
50 import urllib.request, urllib.parse, urllib.error
51 51
52 52 base_url = {
53 53 'repo_summary': '/{repo_name}',
54 54 'repo_archivefile': '/{repo_name}/archive/{fname}',
55 55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
56 56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
57 57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
58 58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
59 59 'repo_files:default_commit': '/{repo_name}/files',
60 60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
61 61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
62 62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
63 63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
64 64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
65 65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
66 66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
67 67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
68 68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
69 69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
70 70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
71 71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
72 72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
73 73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
74 74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
75 75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
76 76 }[name].format(**kwargs)
77 77
78 78 if params:
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
80 80 return base_url
81 81
82 82
83 83 def assert_files_in_response(response, files, params):
84 84 template = (
85 85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
86 86 _assert_items_in_response(response, files, template, params)
87 87
88 88
89 89 def assert_dirs_in_response(response, dirs, params):
90 90 template = (
91 91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
92 92 _assert_items_in_response(response, dirs, template, params)
93 93
94 94
95 95 def _assert_items_in_response(response, items, template, params):
96 96 for item in items:
97 97 item_params = {'name': item}
98 98 item_params.update(params)
99 99 response.mustcontain(template % item_params)
100 100
101 101
102 102 def assert_timeago_in_response(response, items, params):
103 103 for item in items:
104 104 response.mustcontain(h.age_component(params['date']))
105 105
106 106
107 107 @pytest.mark.usefixtures("app")
108 108 class TestFilesViews(object):
109 109
110 110 def test_show_files(self, backend):
111 111 response = self.app.get(
112 112 route_path('repo_files',
113 113 repo_name=backend.repo_name,
114 114 commit_id='tip', f_path='/'))
115 115 commit = backend.repo.get_commit()
116 116
117 117 params = {
118 118 'repo_name': backend.repo_name,
119 119 'commit_id': commit.raw_id,
120 120 'date': commit.date
121 121 }
122 122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
123 123 files = [
124 124 '.gitignore',
125 125 '.hgignore',
126 126 '.hgtags',
127 127 # TODO: missing in Git
128 128 # '.travis.yml',
129 129 'MANIFEST.in',
130 130 'README.rst',
131 131 # TODO: File is missing in svn repository
132 132 # 'run_test_and_report.sh',
133 133 'setup.cfg',
134 134 'setup.py',
135 135 'test_and_report.sh',
136 136 'tox.ini',
137 137 ]
138 138 assert_files_in_response(response, files, params)
139 139 assert_timeago_in_response(response, files, params)
140 140
141 141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
142 142 repo = backend_hg['subrepos']
143 143 response = self.app.get(
144 144 route_path('repo_files',
145 145 repo_name=repo.repo_name,
146 146 commit_id='tip', f_path='/'))
147 147 assert_response = response.assert_response()
148 148 assert_response.contains_one_link(
149 149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
150 150
151 151 def test_show_files_links_submodules_with_absolute_url_subpaths(
152 152 self, backend_hg):
153 153 repo = backend_hg['subrepos']
154 154 response = self.app.get(
155 155 route_path('repo_files',
156 156 repo_name=repo.repo_name,
157 157 commit_id='tip', f_path='/'))
158 158 assert_response = response.assert_response()
159 159 assert_response.contains_one_link(
160 160 'subpaths-path @ 000000000000',
161 161 'http://sub-base.example.com/subpaths-path')
162 162
163 163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
164 164 def test_files_menu(self, backend):
165 165 new_branch = "temp_branch_name"
166 166 commits = [
167 167 {'message': 'a'},
168 168 {'message': 'b', 'branch': new_branch}
169 169 ]
170 170 backend.create_repo(commits)
171 171 backend.repo.landing_rev = "branch:%s" % new_branch
172 172 Session().commit()
173 173
174 174 # get response based on tip and not new commit
175 175 response = self.app.get(
176 176 route_path('repo_files',
177 177 repo_name=backend.repo_name,
178 178 commit_id='tip', f_path='/'))
179 179
180 180 # make sure Files menu url is not tip but new commit
181 181 landing_rev = backend.repo.landing_ref_name
182 182 files_url = route_path('repo_files:default_path',
183 183 repo_name=backend.repo_name,
184 184 commit_id=landing_rev, params={'at': landing_rev})
185 185
186 186 assert landing_rev != 'tip'
187 187 response.mustcontain(
188 188 '<li class="active"><a class="menulink" href="%s">' % files_url)
189 189
190 190 def test_show_files_commit(self, backend):
191 191 commit = backend.repo.get_commit(commit_idx=32)
192 192
193 193 response = self.app.get(
194 194 route_path('repo_files',
195 195 repo_name=backend.repo_name,
196 196 commit_id=commit.raw_id, f_path='/'))
197 197
198 198 dirs = ['docs', 'tests']
199 199 files = ['README.rst']
200 200 params = {
201 201 'repo_name': backend.repo_name,
202 202 'commit_id': commit.raw_id,
203 203 }
204 204 assert_dirs_in_response(response, dirs, params)
205 205 assert_files_in_response(response, files, params)
206 206
207 207 def test_show_files_different_branch(self, backend):
208 208 branches = dict(
209 209 hg=(150, ['git']),
210 210 # TODO: Git test repository does not contain other branches
211 211 git=(633, ['master']),
212 212 # TODO: Branch support in Subversion
213 213 svn=(150, [])
214 214 )
215 215 idx, branches = branches[backend.alias]
216 216 commit = backend.repo.get_commit(commit_idx=idx)
217 217 response = self.app.get(
218 218 route_path('repo_files',
219 219 repo_name=backend.repo_name,
220 220 commit_id=commit.raw_id, f_path='/'))
221 221
222 222 assert_response = response.assert_response()
223 223 for branch in branches:
224 224 assert_response.element_contains('.tags .branchtag', branch)
225 225
226 226 def test_show_files_paging(self, backend):
227 227 repo = backend.repo
228 228 indexes = [73, 92, 109, 1, 0]
229 229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
230 230 for rev in indexes]
231 231
232 232 for idx in idx_map:
233 233 response = self.app.get(
234 234 route_path('repo_files',
235 235 repo_name=backend.repo_name,
236 236 commit_id=idx[1], f_path='/'))
237 237
238 238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
239 239
240 240 def test_file_source(self, backend):
241 241 commit = backend.repo.get_commit(commit_idx=167)
242 242 response = self.app.get(
243 243 route_path('repo_files',
244 244 repo_name=backend.repo_name,
245 245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
246 246
247 247 msgbox = """<div class="commit">%s</div>"""
248 248 response.mustcontain(msgbox % (commit.message, ))
249 249
250 250 assert_response = response.assert_response()
251 251 if commit.branch:
252 252 assert_response.element_contains(
253 253 '.tags.tags-main .branchtag', commit.branch)
254 254 if commit.tags:
255 255 for tag in commit.tags:
256 256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
257 257
258 258 def test_file_source_annotated(self, backend):
259 259 response = self.app.get(
260 260 route_path('repo_files:annotated',
261 261 repo_name=backend.repo_name,
262 262 commit_id='tip', f_path='vcs/nodes.py'))
263 263 expected_commits = {
264 264 'hg': 'r356',
265 265 'git': 'r345',
266 266 'svn': 'r208',
267 267 }
268 268 response.mustcontain(expected_commits[backend.alias])
269 269
270 270 def test_file_source_authors(self, backend):
271 271 response = self.app.get(
272 272 route_path('repo_file_authors',
273 273 repo_name=backend.repo_name,
274 274 commit_id='tip', f_path='vcs/nodes.py'))
275 275 expected_authors = {
276 276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 278 'svn': ('marcin', 'lukasz'),
279 279 }
280 280
281 281 for author in expected_authors[backend.alias]:
282 282 response.mustcontain(author)
283 283
284 284 def test_file_source_authors_with_annotation(self, backend):
285 285 response = self.app.get(
286 286 route_path('repo_file_authors',
287 287 repo_name=backend.repo_name,
288 288 commit_id='tip', f_path='vcs/nodes.py',
289 289 params=dict(annotate=1)))
290 290 expected_authors = {
291 291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 293 'svn': ('marcin', 'lukasz'),
294 294 }
295 295
296 296 for author in expected_authors[backend.alias]:
297 297 response.mustcontain(author)
298 298
299 299 def test_file_source_history(self, backend, xhr_header):
300 300 response = self.app.get(
301 301 route_path('repo_file_history',
302 302 repo_name=backend.repo_name,
303 303 commit_id='tip', f_path='vcs/nodes.py'),
304 304 extra_environ=xhr_header)
305 305 assert get_node_history(backend.alias) == json.loads(response.body)
306 306
307 307 def test_file_source_history_svn(self, backend_svn, xhr_header):
308 308 simple_repo = backend_svn['svn-simple-layout']
309 309 response = self.app.get(
310 310 route_path('repo_file_history',
311 311 repo_name=simple_repo.repo_name,
312 312 commit_id='tip', f_path='trunk/example.py'),
313 313 extra_environ=xhr_header)
314 314
315 315 expected_data = json.loads(
316 316 fixture.load_resource('svn_node_history_branches.json'))
317 317
318 318 assert expected_data == response.json
319 319
320 320 def test_file_source_history_with_annotation(self, backend, xhr_header):
321 321 response = self.app.get(
322 322 route_path('repo_file_history',
323 323 repo_name=backend.repo_name,
324 324 commit_id='tip', f_path='vcs/nodes.py',
325 325 params=dict(annotate=1)),
326 326
327 327 extra_environ=xhr_header)
328 328 assert get_node_history(backend.alias) == json.loads(response.body)
329 329
330 330 def test_tree_search_top_level(self, backend, xhr_header):
331 331 commit = backend.repo.get_commit(commit_idx=173)
332 332 response = self.app.get(
333 333 route_path('repo_files_nodelist',
334 334 repo_name=backend.repo_name,
335 335 commit_id=commit.raw_id, f_path='/'),
336 336 extra_environ=xhr_header)
337 337 assert 'nodes' in response.json
338 338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
339 339
340 340 def test_tree_search_missing_xhr(self, backend):
341 341 self.app.get(
342 342 route_path('repo_files_nodelist',
343 343 repo_name=backend.repo_name,
344 344 commit_id='tip', f_path='/'),
345 345 status=404)
346 346
347 347 def test_tree_search_at_path(self, backend, xhr_header):
348 348 commit = backend.repo.get_commit(commit_idx=173)
349 349 response = self.app.get(
350 350 route_path('repo_files_nodelist',
351 351 repo_name=backend.repo_name,
352 352 commit_id=commit.raw_id, f_path='/docs'),
353 353 extra_environ=xhr_header)
354 354 assert 'nodes' in response.json
355 355 nodes = response.json['nodes']
356 356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
357 357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
358 358
359 359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
360 360 commit = backend.repo.get_commit(commit_idx=173)
361 361 response = self.app.get(
362 362 route_path('repo_files_nodelist',
363 363 repo_name=backend.repo_name,
364 364 commit_id=commit.raw_id, f_path='/docs/api'),
365 365 extra_environ=xhr_header)
366 366 assert 'nodes' in response.json
367 367 nodes = response.json['nodes']
368 368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
369 369
370 370 def test_tree_search_at_path_missing_xhr(self, backend):
371 371 self.app.get(
372 372 route_path('repo_files_nodelist',
373 373 repo_name=backend.repo_name,
374 374 commit_id='tip', f_path='/docs'),
375 375 status=404)
376 376
377 377 def test_nodetree(self, backend, xhr_header):
378 378 commit = backend.repo.get_commit(commit_idx=173)
379 379 response = self.app.get(
380 380 route_path('repo_nodetree_full',
381 381 repo_name=backend.repo_name,
382 382 commit_id=commit.raw_id, f_path='/'),
383 383 extra_environ=xhr_header)
384 384
385 385 assert_response = response.assert_response()
386 386
387 387 for attr in ['data-commit-id', 'data-date', 'data-author']:
388 388 elements = assert_response.get_elements('[{}]'.format(attr))
389 389 assert len(elements) > 1
390 390
391 391 for element in elements:
392 392 assert element.get(attr)
393 393
394 394 def test_nodetree_if_file(self, backend, xhr_header):
395 395 commit = backend.repo.get_commit(commit_idx=173)
396 396 response = self.app.get(
397 397 route_path('repo_nodetree_full',
398 398 repo_name=backend.repo_name,
399 399 commit_id=commit.raw_id, f_path='README.rst'),
400 400 extra_environ=xhr_header)
401 401 assert response.body == ''
402 402
403 403 def test_nodetree_wrong_path(self, backend, xhr_header):
404 404 commit = backend.repo.get_commit(commit_idx=173)
405 405 response = self.app.get(
406 406 route_path('repo_nodetree_full',
407 407 repo_name=backend.repo_name,
408 408 commit_id=commit.raw_id, f_path='/dont-exist'),
409 409 extra_environ=xhr_header)
410 410
411 411 err = 'error: There is no file nor ' \
412 412 'directory at the given path'
413 413 assert err in response.body
414 414
415 415 def test_nodetree_missing_xhr(self, backend):
416 416 self.app.get(
417 417 route_path('repo_nodetree_full',
418 418 repo_name=backend.repo_name,
419 419 commit_id='tip', f_path='/'),
420 420 status=404)
421 421
422 422
423 423 @pytest.mark.usefixtures("app", "autologin_user")
424 424 class TestRawFileHandling(object):
425 425
426 426 def test_download_file(self, backend):
427 427 commit = backend.repo.get_commit(commit_idx=173)
428 428 response = self.app.get(
429 429 route_path('repo_file_download',
430 430 repo_name=backend.repo_name,
431 431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
432 432
433 433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
434 434 assert response.content_type == "text/x-python"
435 435
436 436 def test_download_file_wrong_cs(self, backend):
437 437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
438 438
439 439 response = self.app.get(
440 440 route_path('repo_file_download',
441 441 repo_name=backend.repo_name,
442 442 commit_id=raw_id, f_path='vcs/nodes.svg'),
443 443 status=404)
444 444
445 445 msg = """No such commit exists for this repository"""
446 446 response.mustcontain(msg)
447 447
448 448 def test_download_file_wrong_f_path(self, backend):
449 449 commit = backend.repo.get_commit(commit_idx=173)
450 450 f_path = 'vcs/ERRORnodes.py'
451 451
452 452 response = self.app.get(
453 453 route_path('repo_file_download',
454 454 repo_name=backend.repo_name,
455 455 commit_id=commit.raw_id, f_path=f_path),
456 456 status=404)
457 457
458 458 msg = (
459 459 "There is no file nor directory at the given path: "
460 460 "`%s` at commit %s" % (f_path, commit.short_id))
461 461 response.mustcontain(msg)
462 462
463 463 def test_file_raw(self, backend):
464 464 commit = backend.repo.get_commit(commit_idx=173)
465 465 response = self.app.get(
466 466 route_path('repo_file_raw',
467 467 repo_name=backend.repo_name,
468 468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
469 469
470 470 assert response.content_type == "text/plain"
471 471
472 472 def test_file_raw_binary(self, backend):
473 473 commit = backend.repo.get_commit()
474 474 response = self.app.get(
475 475 route_path('repo_file_raw',
476 476 repo_name=backend.repo_name,
477 477 commit_id=commit.raw_id,
478 478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
479 479
480 480 assert response.content_disposition == 'inline'
481 481
482 482 def test_raw_file_wrong_cs(self, backend):
483 483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
484 484
485 485 response = self.app.get(
486 486 route_path('repo_file_raw',
487 487 repo_name=backend.repo_name,
488 488 commit_id=raw_id, f_path='vcs/nodes.svg'),
489 489 status=404)
490 490
491 491 msg = """No such commit exists for this repository"""
492 492 response.mustcontain(msg)
493 493
494 494 def test_raw_wrong_f_path(self, backend):
495 495 commit = backend.repo.get_commit(commit_idx=173)
496 496 f_path = 'vcs/ERRORnodes.py'
497 497 response = self.app.get(
498 498 route_path('repo_file_raw',
499 499 repo_name=backend.repo_name,
500 500 commit_id=commit.raw_id, f_path=f_path),
501 501 status=404)
502 502
503 503 msg = (
504 504 "There is no file nor directory at the given path: "
505 505 "`%s` at commit %s" % (f_path, commit.short_id))
506 506 response.mustcontain(msg)
507 507
508 508 def test_raw_svg_should_not_be_rendered(self, backend):
509 509 backend.create_repo()
510 510 backend.ensure_file("xss.svg")
511 511 response = self.app.get(
512 512 route_path('repo_file_raw',
513 513 repo_name=backend.repo_name,
514 514 commit_id='tip', f_path='xss.svg'),)
515 515 # If the content type is image/svg+xml then it allows to render HTML
516 516 # and malicious SVG.
517 517 assert response.content_type == "text/plain"
518 518
519 519
520 520 @pytest.mark.usefixtures("app")
521 521 class TestRepositoryArchival(object):
522 522
523 523 def test_archival(self, backend):
524 524 backend.enable_downloads()
525 525 commit = backend.repo.get_commit(commit_idx=173)
526 526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
527 527
528 528 short = commit.short_id + extension
529 529 fname = commit.raw_id + extension
530 530 filename = '%s-%s' % (backend.repo_name, short)
531 531 response = self.app.get(
532 532 route_path('repo_archivefile',
533 533 repo_name=backend.repo_name,
534 534 fname=fname))
535 535
536 536 assert response.status == '200 OK'
537 537 headers = [
538 538 ('Content-Disposition', 'attachment; filename=%s' % filename),
539 539 ('Content-Type', '%s' % content_type),
540 540 ]
541 541
542 542 for header in headers:
543 543 assert header in response.headers.items()
544 544
545 545 def test_archival_no_hash(self, backend):
546 546 backend.enable_downloads()
547 547 commit = backend.repo.get_commit(commit_idx=173)
548 548 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
549 549
550 550 short = 'plain' + extension
551 551 fname = commit.raw_id + extension
552 552 filename = '%s-%s' % (backend.repo_name, short)
553 553 response = self.app.get(
554 554 route_path('repo_archivefile',
555 555 repo_name=backend.repo_name,
556 556 fname=fname, params={'with_hash': 0}))
557 557
558 558 assert response.status == '200 OK'
559 559 headers = [
560 560 ('Content-Disposition', 'attachment; filename=%s' % filename),
561 561 ('Content-Type', '%s' % content_type),
562 562 ]
563 563
564 564 for header in headers:
565 565 assert header in response.headers.items()
566 566
567 567 @pytest.mark.parametrize('arch_ext',[
568 568 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
569 569 def test_archival_wrong_ext(self, backend, arch_ext):
570 570 backend.enable_downloads()
571 571 commit = backend.repo.get_commit(commit_idx=173)
572 572
573 573 fname = commit.raw_id + '.' + arch_ext
574 574
575 575 response = self.app.get(
576 576 route_path('repo_archivefile',
577 577 repo_name=backend.repo_name,
578 578 fname=fname))
579 579 response.mustcontain(
580 580 'Unknown archive type for: `{}`'.format(fname))
581 581
582 582 @pytest.mark.parametrize('commit_id', [
583 583 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
584 584 def test_archival_wrong_commit_id(self, backend, commit_id):
585 585 backend.enable_downloads()
586 586 fname = '%s.zip' % commit_id
587 587
588 588 response = self.app.get(
589 589 route_path('repo_archivefile',
590 590 repo_name=backend.repo_name,
591 591 fname=fname))
592 592 response.mustcontain('Unknown commit_id')
593 593
594 594
595 595 @pytest.mark.usefixtures("app")
596 596 class TestFilesDiff(object):
597 597
598 598 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
599 599 def test_file_full_diff(self, backend, diff):
600 600 commit1 = backend.repo.get_commit(commit_idx=-1)
601 601 commit2 = backend.repo.get_commit(commit_idx=-2)
602 602
603 603 response = self.app.get(
604 604 route_path('repo_files_diff',
605 605 repo_name=backend.repo_name,
606 606 f_path='README'),
607 607 params={
608 608 'diff1': commit2.raw_id,
609 609 'diff2': commit1.raw_id,
610 610 'fulldiff': '1',
611 611 'diff': diff,
612 612 })
613 613
614 614 if diff == 'diff':
615 615 # use redirect since this is OLD view redirecting to compare page
616 616 response = response.follow()
617 617
618 618 # It's a symlink to README.rst
619 619 response.mustcontain('README.rst')
620 620 response.mustcontain('No newline at end of file')
621 621
622 622 def test_file_binary_diff(self, backend):
623 623 commits = [
624 624 {'message': 'First commit'},
625 625 {'message': 'Commit with binary',
626 626 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
627 627 ]
628 628 repo = backend.create_repo(commits=commits)
629 629
630 630 response = self.app.get(
631 631 route_path('repo_files_diff',
632 632 repo_name=backend.repo_name,
633 633 f_path='file.bin'),
634 634 params={
635 635 'diff1': repo.get_commit(commit_idx=0).raw_id,
636 636 'diff2': repo.get_commit(commit_idx=1).raw_id,
637 637 'fulldiff': '1',
638 638 'diff': 'diff',
639 639 })
640 640 # use redirect since this is OLD view redirecting to compare page
641 641 response = response.follow()
642 642 response.mustcontain('Collapse 1 commit')
643 643 file_changes = (1, 0, 0)
644 644
645 645 compare_page = ComparePage(response)
646 646 compare_page.contains_change_summary(*file_changes)
647 647
648 648 if backend.alias == 'svn':
649 649 response.mustcontain('new file 10644')
650 650 # TODO(marcink): SVN doesn't yet detect binary changes
651 651 else:
652 652 response.mustcontain('new file 100644')
653 653 response.mustcontain('binary diff hidden')
654 654
655 655 def test_diff_2way(self, backend):
656 656 commit1 = backend.repo.get_commit(commit_idx=-1)
657 657 commit2 = backend.repo.get_commit(commit_idx=-2)
658 658 response = self.app.get(
659 659 route_path('repo_files_diff_2way_redirect',
660 660 repo_name=backend.repo_name,
661 661 f_path='README'),
662 662 params={
663 663 'diff1': commit2.raw_id,
664 664 'diff2': commit1.raw_id,
665 665 })
666 666 # use redirect since this is OLD view redirecting to compare page
667 667 response = response.follow()
668 668
669 669 # It's a symlink to README.rst
670 670 response.mustcontain('README.rst')
671 671 response.mustcontain('No newline at end of file')
672 672
673 673 def test_requires_one_commit_id(self, backend, autologin_user):
674 674 response = self.app.get(
675 675 route_path('repo_files_diff',
676 676 repo_name=backend.repo_name,
677 677 f_path='README.rst'),
678 678 status=400)
679 679 response.mustcontain(
680 680 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
681 681
682 682 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
683 683 repo = vcsbackend.repo
684 684 response = self.app.get(
685 685 route_path('repo_files_diff',
686 686 repo_name=repo.name,
687 687 f_path='does-not-exist-in-any-commit'),
688 688 params={
689 689 'diff1': repo[0].raw_id,
690 690 'diff2': repo[1].raw_id
691 691 })
692 692
693 693 response = response.follow()
694 694 response.mustcontain('No files')
695 695
696 696 def test_returns_redirect_if_file_not_changed(self, backend):
697 697 commit = backend.repo.get_commit(commit_idx=-1)
698 698 response = self.app.get(
699 699 route_path('repo_files_diff_2way_redirect',
700 700 repo_name=backend.repo_name,
701 701 f_path='README'),
702 702 params={
703 703 'diff1': commit.raw_id,
704 704 'diff2': commit.raw_id,
705 705 })
706 706
707 707 response = response.follow()
708 708 response.mustcontain('No files')
709 709 response.mustcontain('No commits in this compare')
710 710
711 711 def test_supports_diff_to_different_path_svn(self, backend_svn):
712 712 #TODO: check this case
713 713 return
714 714
715 715 repo = backend_svn['svn-simple-layout'].scm_instance()
716 716 commit_id_1 = '24'
717 717 commit_id_2 = '26'
718 718
719 719 response = self.app.get(
720 720 route_path('repo_files_diff',
721 721 repo_name=backend_svn.repo_name,
722 722 f_path='trunk/example.py'),
723 723 params={
724 724 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
725 725 'diff2': commit_id_2,
726 726 })
727 727
728 728 response = response.follow()
729 729 response.mustcontain(
730 730 # diff contains this
731 731 "Will print out a useful message on invocation.")
732 732
733 733 # Note: Expecting that we indicate the user what's being compared
734 734 response.mustcontain("trunk/example.py")
735 735 response.mustcontain("tags/v0.2/example.py")
736 736
737 737 def test_show_rev_redirects_to_svn_path(self, backend_svn):
738 738 #TODO: check this case
739 739 return
740 740
741 741 repo = backend_svn['svn-simple-layout'].scm_instance()
742 742 commit_id = repo[-1].raw_id
743 743
744 744 response = self.app.get(
745 745 route_path('repo_files_diff',
746 746 repo_name=backend_svn.repo_name,
747 747 f_path='trunk/example.py'),
748 748 params={
749 749 'diff1': 'branches/argparse/example.py@' + commit_id,
750 750 'diff2': commit_id,
751 751 },
752 752 status=302)
753 753 response = response.follow()
754 754 assert response.headers['Location'].endswith(
755 755 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
756 756
757 757 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
758 758 #TODO: check this case
759 759 return
760 760
761 761 repo = backend_svn['svn-simple-layout'].scm_instance()
762 762 commit_id = repo[-1].raw_id
763 763 response = self.app.get(
764 764 route_path('repo_files_diff',
765 765 repo_name=backend_svn.repo_name,
766 766 f_path='trunk/example.py'),
767 767 params={
768 768 'diff1': 'branches/argparse/example.py@' + commit_id,
769 769 'diff2': commit_id,
770 770 'show_rev': 'Show at Revision',
771 771 'annotate': 'true',
772 772 },
773 773 status=302)
774 774 response = response.follow()
775 775 assert response.headers['Location'].endswith(
776 776 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
777 777
778 778
779 779 @pytest.mark.usefixtures("app", "autologin_user")
780 780 class TestModifyFilesWithWebInterface(object):
781 781
782 782 def test_add_file_view(self, backend):
783 783 self.app.get(
784 784 route_path('repo_files_add_file',
785 785 repo_name=backend.repo_name,
786 786 commit_id='tip', f_path='/')
787 787 )
788 788
789 789 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
790 790 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
791 791 backend.create_repo()
792 792 filename = 'init.py'
793 793 response = self.app.post(
794 794 route_path('repo_files_create_file',
795 795 repo_name=backend.repo_name,
796 796 commit_id='tip', f_path='/'),
797 797 params={
798 798 'content': "",
799 799 'filename': filename,
800 800 'csrf_token': csrf_token,
801 801 },
802 802 status=302)
803 803 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
804 804 assert_session_flash(response, expected_msg)
805 805
806 806 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
807 807 commit_id = backend.repo.get_commit().raw_id
808 808 response = self.app.post(
809 809 route_path('repo_files_create_file',
810 810 repo_name=backend.repo_name,
811 811 commit_id=commit_id, f_path='/'),
812 812 params={
813 813 'content': "foo",
814 814 'csrf_token': csrf_token,
815 815 },
816 816 status=302)
817 817
818 818 assert_session_flash(response, 'No filename specified')
819 819
820 820 def test_add_file_into_repo_errors_and_no_commits(
821 821 self, backend, csrf_token):
822 822 repo = backend.create_repo()
823 823 # Create a file with no filename, it will display an error but
824 824 # the repo has no commits yet
825 825 response = self.app.post(
826 826 route_path('repo_files_create_file',
827 827 repo_name=repo.repo_name,
828 828 commit_id='tip', f_path='/'),
829 829 params={
830 830 'content': "foo",
831 831 'csrf_token': csrf_token,
832 832 },
833 833 status=302)
834 834
835 835 assert_session_flash(response, 'No filename specified')
836 836
837 837 # Not allowed, redirect to the summary
838 838 redirected = response.follow()
839 839 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
840 840
841 841 # As there are no commits, displays the summary page with the error of
842 842 # creating a file with no filename
843 843
844 844 assert redirected.request.path == summary_url
845 845
846 846 @pytest.mark.parametrize("filename, clean_filename", [
847 847 ('/abs/foo', 'abs/foo'),
848 848 ('../rel/foo', 'rel/foo'),
849 849 ('file/../foo/foo', 'file/foo/foo'),
850 850 ])
851 851 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
852 852 repo = backend.create_repo()
853 853 commit_id = repo.get_commit().raw_id
854 854
855 855 response = self.app.post(
856 856 route_path('repo_files_create_file',
857 857 repo_name=repo.repo_name,
858 858 commit_id=commit_id, f_path='/'),
859 859 params={
860 860 'content': "foo",
861 861 'filename': filename,
862 862 'csrf_token': csrf_token,
863 863 },
864 864 status=302)
865 865
866 866 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
867 867 assert_session_flash(response, expected_msg)
868 868
869 869 @pytest.mark.parametrize("cnt, filename, content", [
870 870 (1, 'foo.txt', "Content"),
871 871 (2, 'dir/foo.rst', "Content"),
872 872 (3, 'dir/foo-second.rst', "Content"),
873 873 (4, 'rel/dir/foo.bar', "Content"),
874 874 ])
875 875 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
876 876 repo = backend.create_repo()
877 877 commit_id = repo.get_commit().raw_id
878 878 response = self.app.post(
879 879 route_path('repo_files_create_file',
880 880 repo_name=repo.repo_name,
881 881 commit_id=commit_id, f_path='/'),
882 882 params={
883 883 'content': content,
884 884 'filename': filename,
885 885 'csrf_token': csrf_token,
886 886 },
887 887 status=302)
888 888
889 889 expected_msg = 'Successfully committed new file `{}`'.format(filename)
890 890 assert_session_flash(response, expected_msg)
891 891
892 892 def test_edit_file_view(self, backend):
893 893 response = self.app.get(
894 894 route_path('repo_files_edit_file',
895 895 repo_name=backend.repo_name,
896 896 commit_id=backend.default_head_id,
897 897 f_path='vcs/nodes.py'),
898 898 status=200)
899 899 response.mustcontain("Module holding everything related to vcs nodes.")
900 900
901 901 def test_edit_file_view_not_on_branch(self, backend):
902 902 repo = backend.create_repo()
903 903 backend.ensure_file("vcs/nodes.py")
904 904
905 905 response = self.app.get(
906 906 route_path('repo_files_edit_file',
907 907 repo_name=repo.repo_name,
908 908 commit_id='tip',
909 909 f_path='vcs/nodes.py'),
910 910 status=302)
911 911 assert_session_flash(
912 912 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
913 913
914 914 def test_edit_file_view_commit_changes(self, backend, csrf_token):
915 915 repo = backend.create_repo()
916 916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
917 917
918 918 response = self.app.post(
919 919 route_path('repo_files_update_file',
920 920 repo_name=repo.repo_name,
921 921 commit_id=backend.default_head_id,
922 922 f_path='vcs/nodes.py'),
923 923 params={
924 924 'content': "print 'hello world'",
925 925 'message': 'I committed',
926 926 'filename': "vcs/nodes.py",
927 927 'csrf_token': csrf_token,
928 928 },
929 929 status=302)
930 930 assert_session_flash(
931 931 response, 'Successfully committed changes to file `vcs/nodes.py`')
932 932 tip = repo.get_commit(commit_idx=-1)
933 933 assert tip.message == 'I committed'
934 934
935 935 def test_edit_file_view_commit_changes_default_message(self, backend,
936 936 csrf_token):
937 937 repo = backend.create_repo()
938 938 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
939 939
940 940 commit_id = (
941 941 backend.default_branch_name or
942 942 backend.repo.scm_instance().commit_ids[-1])
943 943
944 944 response = self.app.post(
945 945 route_path('repo_files_update_file',
946 946 repo_name=repo.repo_name,
947 947 commit_id=commit_id,
948 948 f_path='vcs/nodes.py'),
949 949 params={
950 950 'content': "print 'hello world'",
951 951 'message': '',
952 952 'filename': "vcs/nodes.py",
953 953 'csrf_token': csrf_token,
954 954 },
955 955 status=302)
956 956 assert_session_flash(
957 957 response, 'Successfully committed changes to file `vcs/nodes.py`')
958 958 tip = repo.get_commit(commit_idx=-1)
959 959 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
960 960
961 961 def test_delete_file_view(self, backend):
962 962 self.app.get(
963 963 route_path('repo_files_remove_file',
964 964 repo_name=backend.repo_name,
965 965 commit_id=backend.default_head_id,
966 966 f_path='vcs/nodes.py'),
967 967 status=200)
968 968
969 969 def test_delete_file_view_not_on_branch(self, backend):
970 970 repo = backend.create_repo()
971 971 backend.ensure_file('vcs/nodes.py')
972 972
973 973 response = self.app.get(
974 974 route_path('repo_files_remove_file',
975 975 repo_name=repo.repo_name,
976 976 commit_id='tip',
977 977 f_path='vcs/nodes.py'),
978 978 status=302)
979 979 assert_session_flash(
980 980 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
981 981
982 982 def test_delete_file_view_commit_changes(self, backend, csrf_token):
983 983 repo = backend.create_repo()
984 984 backend.ensure_file("vcs/nodes.py")
985 985
986 986 response = self.app.post(
987 987 route_path('repo_files_delete_file',
988 988 repo_name=repo.repo_name,
989 989 commit_id=backend.default_head_id,
990 990 f_path='vcs/nodes.py'),
991 991 params={
992 992 'message': 'i committed',
993 993 'csrf_token': csrf_token,
994 994 },
995 995 status=302)
996 996 assert_session_flash(
997 997 response, 'Successfully deleted file `vcs/nodes.py`')
998 998
999 999
1000 1000 @pytest.mark.usefixtures("app")
1001 1001 class TestFilesViewOtherCases(object):
1002 1002
1003 1003 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1004 1004 self, backend_stub, autologin_regular_user, user_regular,
1005 1005 user_util):
1006 1006
1007 1007 repo = backend_stub.create_repo()
1008 1008 user_util.grant_user_permission_to_repo(
1009 1009 repo, user_regular, 'repository.write')
1010 1010 response = self.app.get(
1011 1011 route_path('repo_files',
1012 1012 repo_name=repo.repo_name,
1013 1013 commit_id='tip', f_path='/'))
1014 1014
1015 1015 repo_file_add_url = route_path(
1016 1016 'repo_files_add_file',
1017 1017 repo_name=repo.repo_name,
1018 1018 commit_id=0, f_path='')
1019 1019
1020 1020 assert_session_flash(
1021 1021 response,
1022 1022 'There are no files yet. <a class="alert-link" '
1023 1023 'href="{}">Click here to add a new file.</a>'
1024 1024 .format(repo_file_add_url))
1025 1025
1026 1026 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1027 1027 self, backend_stub, autologin_regular_user):
1028 1028 repo = backend_stub.create_repo()
1029 1029 # init session for anon user
1030 1030 route_path('repo_summary', repo_name=repo.repo_name)
1031 1031
1032 1032 repo_file_add_url = route_path(
1033 1033 'repo_files_add_file',
1034 1034 repo_name=repo.repo_name,
1035 1035 commit_id=0, f_path='')
1036 1036
1037 1037 response = self.app.get(
1038 1038 route_path('repo_files',
1039 1039 repo_name=repo.repo_name,
1040 1040 commit_id='tip', f_path='/'))
1041 1041
1042 1042 assert_session_flash(response, no_=repo_file_add_url)
1043 1043
1044 1044 @pytest.mark.parametrize('file_node', [
1045 1045 'archive/file.zip',
1046 1046 'diff/my-file.txt',
1047 1047 'render.py',
1048 1048 'render',
1049 1049 'remove_file',
1050 1050 'remove_file/to-delete.txt',
1051 1051 ])
1052 1052 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1053 1053 backend.create_repo()
1054 1054 backend.ensure_file(file_node)
1055 1055
1056 1056 self.app.get(
1057 1057 route_path('repo_files',
1058 1058 repo_name=backend.repo_name,
1059 1059 commit_id='tip', f_path=file_node),
1060 1060 status=200)
1061 1061
1062 1062
1063 1063 class TestAdjustFilePathForSvn(object):
1064 1064 """
1065 1065 SVN specific adjustments of node history in RepoFilesView.
1066 1066 """
1067 1067
1068 1068 def test_returns_path_relative_to_matched_reference(self):
1069 1069 repo = self._repo(branches=['trunk'])
1070 1070 self.assert_file_adjustment('trunk/file', 'file', repo)
1071 1071
1072 1072 def test_does_not_modify_file_if_no_reference_matches(self):
1073 1073 repo = self._repo(branches=['trunk'])
1074 1074 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1075 1075
1076 1076 def test_does_not_adjust_partial_directory_names(self):
1077 1077 repo = self._repo(branches=['trun'])
1078 1078 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1079 1079
1080 1080 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1081 1081 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1082 1082 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1083 1083
1084 1084 def assert_file_adjustment(self, f_path, expected, repo):
1085 1085 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1086 1086 assert result == expected
1087 1087
1088 1088 def _repo(self, branches=None):
1089 1089 repo = mock.Mock()
1090 1090 repo.branches = OrderedDict((name, '0') for name in branches or [])
1091 1091 repo.tags = {}
1092 1092 return repo
@@ -1,333 +1,333 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
24 24
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.lib import helpers as h
27 27
28 28 from rhodecode.model.db import Repository
29 29 from rhodecode.model.repo import RepoModel
30 30 from rhodecode.model.user import UserModel
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
37 import urllib.request, urllib.parse, urllib.error
38 38
39 39 base_url = {
40 40 'repo_summary': '/{repo_name}',
41 41 'repo_creating_check': '/{repo_name}/repo_creating_check',
42 42 'repo_fork_new': '/{repo_name}/fork',
43 43 'repo_fork_create': '/{repo_name}/fork/create',
44 44 'repo_forks_show_all': '/{repo_name}/forks',
45 45 'repo_forks_data': '/{repo_name}/forks/data',
46 46 }[name].format(**kwargs)
47 47
48 48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 50 return base_url
51 51
52 52
53 53 FORK_NAME = {
54 54 'hg': HG_FORK,
55 55 'git': GIT_FORK
56 56 }
57 57
58 58
59 59 @pytest.mark.skip_backends('svn')
60 60 class TestRepoForkViewTests(TestController):
61 61
62 62 def test_show_forks(self, backend, xhr_header):
63 63 self.log_user()
64 64 response = self.app.get(
65 65 route_path('repo_forks_data', repo_name=backend.repo_name),
66 66 extra_environ=xhr_header)
67 67
68 68 assert response.json == {u'data': [], u'draw': None,
69 69 u'recordsFiltered': 0, u'recordsTotal': 0}
70 70
71 71 def test_no_permissions_to_fork_page(self, backend, user_util):
72 72 user = user_util.create_user(password='qweqwe')
73 73 user_id = user.user_id
74 74 self.log_user(user.username, 'qweqwe')
75 75
76 76 user_model = UserModel()
77 77 user_model.revoke_perm(user_id, 'hg.fork.repository')
78 78 user_model.grant_perm(user_id, 'hg.fork.none')
79 79 u = UserModel().get(user_id)
80 80 u.inherit_default_permissions = False
81 81 Session().commit()
82 82 # try create a fork
83 83 self.app.get(
84 84 route_path('repo_fork_new', repo_name=backend.repo_name),
85 85 status=404)
86 86
87 87 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
88 88 user = user_util.create_user(password='qweqwe')
89 89 user_id = user.user_id
90 90 self.log_user(user.username, 'qweqwe')
91 91
92 92 user_model = UserModel()
93 93 user_model.revoke_perm(user_id, 'hg.fork.repository')
94 94 user_model.grant_perm(user_id, 'hg.fork.none')
95 95 u = UserModel().get(user_id)
96 96 u.inherit_default_permissions = False
97 97 Session().commit()
98 98 # try create a fork
99 99 self.app.post(
100 100 route_path('repo_fork_create', repo_name=backend.repo_name),
101 101 {'csrf_token': csrf_token},
102 102 status=404)
103 103
104 104 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
105 105 # try create a fork
106 106 response = self.app.post(
107 107 route_path('repo_fork_create', repo_name=backend.repo_name),
108 108 {'csrf_token': csrf_token},
109 109 status=200)
110 110 # test if html fill works fine
111 111 response.mustcontain('Missing value')
112 112
113 113 def test_create_fork_page(self, autologin_user, backend):
114 114 self.app.get(
115 115 route_path('repo_fork_new', repo_name=backend.repo_name),
116 116 status=200)
117 117
118 118 def test_create_and_show_fork(
119 119 self, autologin_user, backend, csrf_token, xhr_header):
120 120
121 121 # create a fork
122 122 fork_name = FORK_NAME[backend.alias]
123 123 description = 'fork of vcs test'
124 124 repo_name = backend.repo_name
125 125 source_repo = Repository.get_by_repo_name(repo_name)
126 126 creation_args = {
127 127 'repo_name': fork_name,
128 128 'repo_group': '',
129 129 'fork_parent_id': source_repo.repo_id,
130 130 'repo_type': backend.alias,
131 131 'description': description,
132 132 'private': 'False',
133 133 'csrf_token': csrf_token,
134 134 }
135 135
136 136 self.app.post(
137 137 route_path('repo_fork_create', repo_name=repo_name), creation_args)
138 138
139 139 response = self.app.get(
140 140 route_path('repo_forks_data', repo_name=repo_name),
141 141 extra_environ=xhr_header)
142 142
143 143 assert response.json['data'][0]['fork_name'] == \
144 144 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
145 145
146 146 # remove this fork
147 147 fixture.destroy_repo(fork_name)
148 148
149 149 def test_fork_create(self, autologin_user, backend, csrf_token):
150 150 fork_name = FORK_NAME[backend.alias]
151 151 description = 'fork of vcs test'
152 152 repo_name = backend.repo_name
153 153 source_repo = Repository.get_by_repo_name(repo_name)
154 154 creation_args = {
155 155 'repo_name': fork_name,
156 156 'repo_group': '',
157 157 'fork_parent_id': source_repo.repo_id,
158 158 'repo_type': backend.alias,
159 159 'description': description,
160 160 'private': 'False',
161 161 'csrf_token': csrf_token,
162 162 }
163 163 self.app.post(
164 164 route_path('repo_fork_create', repo_name=repo_name), creation_args)
165 165 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
166 166 assert repo.fork.repo_name == backend.repo_name
167 167
168 168 # run the check page that triggers the flash message
169 169 response = self.app.get(
170 170 route_path('repo_creating_check', repo_name=fork_name))
171 171 # test if we have a message that fork is ok
172 172 assert_session_flash(response,
173 173 'Forked repository %s as <a href="/%s">%s</a>' % (
174 174 repo_name, fork_name, fork_name))
175 175
176 176 # test if the fork was created in the database
177 177 fork_repo = Session().query(Repository)\
178 178 .filter(Repository.repo_name == fork_name).one()
179 179
180 180 assert fork_repo.repo_name == fork_name
181 181 assert fork_repo.fork.repo_name == repo_name
182 182
183 183 # test if the repository is visible in the list ?
184 184 response = self.app.get(
185 185 h.route_path('repo_summary', repo_name=fork_name))
186 186 response.mustcontain(fork_name)
187 187 response.mustcontain(backend.alias)
188 188 response.mustcontain('Fork of')
189 189 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
190 190
191 191 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
192 192 group = fixture.create_repo_group('vc')
193 193 group_id = group.group_id
194 194 fork_name = FORK_NAME[backend.alias]
195 195 fork_name_full = 'vc/%s' % fork_name
196 196 description = 'fork of vcs test'
197 197 repo_name = backend.repo_name
198 198 source_repo = Repository.get_by_repo_name(repo_name)
199 199 creation_args = {
200 200 'repo_name': fork_name,
201 201 'repo_group': group_id,
202 202 'fork_parent_id': source_repo.repo_id,
203 203 'repo_type': backend.alias,
204 204 'description': description,
205 205 'private': 'False',
206 206 'csrf_token': csrf_token,
207 207 }
208 208 self.app.post(
209 209 route_path('repo_fork_create', repo_name=repo_name), creation_args)
210 210 repo = Repository.get_by_repo_name(fork_name_full)
211 211 assert repo.fork.repo_name == backend.repo_name
212 212
213 213 # run the check page that triggers the flash message
214 214 response = self.app.get(
215 215 route_path('repo_creating_check', repo_name=fork_name_full))
216 216 # test if we have a message that fork is ok
217 217 assert_session_flash(response,
218 218 'Forked repository %s as <a href="/%s">%s</a>' % (
219 219 repo_name, fork_name_full, fork_name_full))
220 220
221 221 # test if the fork was created in the database
222 222 fork_repo = Session().query(Repository)\
223 223 .filter(Repository.repo_name == fork_name_full).one()
224 224
225 225 assert fork_repo.repo_name == fork_name_full
226 226 assert fork_repo.fork.repo_name == repo_name
227 227
228 228 # test if the repository is visible in the list ?
229 229 response = self.app.get(
230 230 h.route_path('repo_summary', repo_name=fork_name_full))
231 231 response.mustcontain(fork_name_full)
232 232 response.mustcontain(backend.alias)
233 233
234 234 response.mustcontain('Fork of')
235 235 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
236 236
237 237 fixture.destroy_repo(fork_name_full)
238 238 fixture.destroy_repo_group(group_id)
239 239
240 240 def test_fork_read_permission(self, backend, xhr_header, user_util):
241 241 user = user_util.create_user(password='qweqwe')
242 242 user_id = user.user_id
243 243 self.log_user(user.username, 'qweqwe')
244 244
245 245 # create a fake fork
246 246 fork = user_util.create_repo(repo_type=backend.alias)
247 247 source = user_util.create_repo(repo_type=backend.alias)
248 248 repo_name = source.repo_name
249 249
250 250 fork.fork_id = source.repo_id
251 251 fork_name = fork.repo_name
252 252 Session().commit()
253 253
254 254 forks = Repository.query()\
255 255 .filter(Repository.repo_type == backend.alias)\
256 256 .filter(Repository.fork_id == source.repo_id).all()
257 257 assert 1 == len(forks)
258 258
259 259 # set read permissions for this
260 260 RepoModel().grant_user_permission(
261 261 repo=forks[0], user=user_id, perm='repository.read')
262 262 Session().commit()
263 263
264 264 response = self.app.get(
265 265 route_path('repo_forks_data', repo_name=repo_name),
266 266 extra_environ=xhr_header)
267 267
268 268 assert response.json['data'][0]['fork_name'] == \
269 269 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
270 270
271 271 def test_fork_none_permission(self, backend, xhr_header, user_util):
272 272 user = user_util.create_user(password='qweqwe')
273 273 user_id = user.user_id
274 274 self.log_user(user.username, 'qweqwe')
275 275
276 276 # create a fake fork
277 277 fork = user_util.create_repo(repo_type=backend.alias)
278 278 source = user_util.create_repo(repo_type=backend.alias)
279 279 repo_name = source.repo_name
280 280
281 281 fork.fork_id = source.repo_id
282 282
283 283 Session().commit()
284 284
285 285 forks = Repository.query()\
286 286 .filter(Repository.repo_type == backend.alias)\
287 287 .filter(Repository.fork_id == source.repo_id).all()
288 288 assert 1 == len(forks)
289 289
290 290 # set none
291 291 RepoModel().grant_user_permission(
292 292 repo=forks[0], user=user_id, perm='repository.none')
293 293 Session().commit()
294 294
295 295 # fork shouldn't be there
296 296 response = self.app.get(
297 297 route_path('repo_forks_data', repo_name=repo_name),
298 298 extra_environ=xhr_header)
299 299
300 300 assert response.json == {u'data': [], u'draw': None,
301 301 u'recordsFiltered': 0, u'recordsTotal': 0}
302 302
303 303 @pytest.mark.parametrize('url_type', [
304 304 'repo_fork_new',
305 305 'repo_fork_create'
306 306 ])
307 307 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
308 308 user = user_util.create_user(password='qweqwe')
309 309 self.log_user(user.username, 'qweqwe')
310 310
311 311 # create a temporary repo
312 312 source = user_util.create_repo(repo_type=backend.alias)
313 313 repo_name = source.repo_name
314 314 repo = Repository.get_by_repo_name(repo_name)
315 315 repo.archived = True
316 316 Session().commit()
317 317
318 318 response = self.app.get(
319 319 route_path(url_type, repo_name=repo_name), status=302)
320 320
321 321 msg = 'Action not supported for archived repository.'
322 322 assert_session_flash(response, msg)
323 323
324 324
325 325 class TestSVNFork(TestController):
326 326 @pytest.mark.parametrize('route_name', [
327 327 'repo_fork_create', 'repo_fork_new'
328 328 ])
329 329 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
330 330
331 331 self.app.get(route_path(
332 332 route_name, repo_name=backend_svn.repo_name),
333 333 status=404)
@@ -1,149 +1,149 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.lib.utils2 import md5
24 24 from rhodecode.model.db import Repository
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
27 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 import urllib
30 import urllib.request, urllib.parse, urllib.error
31 31
32 32 base_url = {
33 33 'repo_summary': '/{repo_name}',
34 34 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
35 35 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
36 36 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
37 37 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
38 38 }[name].format(**kwargs)
39 39
40 40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 42 return base_url
43 43
44 44
45 45 @pytest.mark.usefixtures("app")
46 46 class TestRepoIssueTracker(object):
47 47 def test_issuetracker_index(self, autologin_user, backend):
48 48 repo = backend.create_repo()
49 49 response = self.app.get(route_path('edit_repo_issuetracker',
50 50 repo_name=repo.repo_name))
51 51 assert response.status_code == 200
52 52
53 53 def test_add_and_test_issuetracker_patterns(
54 54 self, autologin_user, backend, csrf_token, request, xhr_header):
55 55 pattern = 'issuetracker_pat'
56 56 another_pattern = pattern+'1'
57 57 post_url = route_path(
58 58 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
59 59 post_data = {
60 60 'new_pattern_pattern_0': pattern,
61 61 'new_pattern_url_0': 'http://url',
62 62 'new_pattern_prefix_0': 'prefix',
63 63 'new_pattern_description_0': 'description',
64 64 'new_pattern_pattern_1': another_pattern,
65 65 'new_pattern_url_1': '/url1',
66 66 'new_pattern_prefix_1': 'prefix1',
67 67 'new_pattern_description_1': 'description1',
68 68 'csrf_token': csrf_token
69 69 }
70 70 self.app.post(post_url, post_data, status=302)
71 71 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
72 72 settings = self.settings_model.get_repo_settings()
73 73 self.uid = md5(pattern)
74 74 assert settings[self.uid]['pat'] == pattern
75 75 self.another_uid = md5(another_pattern)
76 76 assert settings[self.another_uid]['pat'] == another_pattern
77 77
78 78 # test pattern
79 79 data = {'test_text': 'example of issuetracker_pat replacement',
80 80 'csrf_token': csrf_token}
81 81 response = self.app.post(
82 82 route_path('edit_repo_issuetracker_test',
83 83 repo_name=backend.repo.repo_name),
84 84 extra_environ=xhr_header, params=data)
85 85
86 86 assert response.body == \
87 87 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
88 88
89 89 @request.addfinalizer
90 90 def cleanup():
91 91 self.settings_model.delete_entries(self.uid)
92 92 self.settings_model.delete_entries(self.another_uid)
93 93
94 94 def test_edit_issuetracker_pattern(
95 95 self, autologin_user, backend, csrf_token, request):
96 96 entry_key = 'issuetracker_pat_'
97 97 pattern = 'issuetracker_pat2'
98 98 old_pattern = 'issuetracker_pat'
99 99 old_uid = md5(old_pattern)
100 100
101 101 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
102 102 entry_key+old_uid, old_pattern, 'unicode')
103 103 Session().add(sett)
104 104 Session().commit()
105 105 post_url = route_path(
106 106 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
107 107 post_data = {
108 108 'new_pattern_pattern_0': pattern,
109 109 'new_pattern_url_0': '/url',
110 110 'new_pattern_prefix_0': 'prefix',
111 111 'new_pattern_description_0': 'description',
112 112 'uid': old_uid,
113 113 'csrf_token': csrf_token
114 114 }
115 115 self.app.post(post_url, post_data, status=302)
116 116 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
117 117 settings = self.settings_model.get_repo_settings()
118 118 self.uid = md5(pattern)
119 119 assert settings[self.uid]['pat'] == pattern
120 120 with pytest.raises(KeyError):
121 121 key = settings[old_uid]
122 122
123 123 @request.addfinalizer
124 124 def cleanup():
125 125 self.settings_model.delete_entries(self.uid)
126 126
127 127 def test_delete_issuetracker_pattern(
128 128 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
129 129 repo = backend.create_repo()
130 130 repo_name = repo.repo_name
131 131 entry_key = 'issuetracker_pat_'
132 132 pattern = 'issuetracker_pat3'
133 133 uid = md5(pattern)
134 134 settings_util.create_repo_rhodecode_setting(
135 135 repo=backend.repo, name=entry_key+uid,
136 136 value=entry_key, type_='unicode', cleanup=False)
137 137
138 138 self.app.post(
139 139 route_path(
140 140 'edit_repo_issuetracker_delete',
141 141 repo_name=backend.repo.repo_name),
142 142 {
143 143 'uid': uid,
144 144 'csrf_token': csrf_token,
145 145 '': ''
146 146 }, extra_environ=xhr_header, status=200)
147 147 settings = IssueTrackerSettingsModel(
148 148 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
149 149 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,74 +1,74 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.lib.utils2 import str2bool
25 25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.tests import (
29 29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
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
36 import urllib.request, urllib.parse, urllib.error
37 37
38 38 base_url = {
39 39 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
40 40 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
41 41
42 42 }[name].format(**kwargs)
43 43
44 44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 46 return base_url
47 47
48 48
49 49 def _get_permission_for_user(user, repo):
50 50 perm = UserRepoToPerm.query()\
51 51 .filter(UserRepoToPerm.repository ==
52 52 Repository.get_by_repo_name(repo))\
53 53 .filter(UserRepoToPerm.user == User.get_by_username(user))\
54 54 .all()
55 55 return perm
56 56
57 57
58 58 @pytest.mark.usefixtures('autologin_user', 'app')
59 59 class TestAdminRepoMaintenance(object):
60 60 @pytest.mark.parametrize('urlname', [
61 61 'edit_repo_maintenance',
62 62 ])
63 63 def test_show_page(self, urlname, app, backend):
64 64 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
65 65
66 66 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
67 67 repo_name = backend_hg.repo_name
68 68
69 69 response = app.get(
70 70 route_path('edit_repo_maintenance_execute',
71 71 repo_name=repo_name,),
72 72 extra_environ=xhr_header)
73 73
74 74 assert "HG Verify repo" in ''.join(response.json)
@@ -1,77 +1,77 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import pytest
22 22
23 23 from rhodecode.tests.utils import permission_update_data_generator
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28 28
29 29 base_url = {
30 30 'edit_repo_perms': '/{repo_name}/settings/permissions'
31 31 # update is the same url
32 32 }[name].format(**kwargs)
33 33
34 34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 36 return base_url
37 37
38 38
39 39 @pytest.mark.usefixtures("app")
40 40 class TestRepoPermissionsView(object):
41 41
42 42 def test_edit_perms_view(self, user_util, autologin_user):
43 43 repo = user_util.create_repo()
44 44 self.app.get(
45 45 route_path('edit_repo_perms',
46 46 repo_name=repo.repo_name), status=200)
47 47
48 48 def test_update_permissions(self, csrf_token, user_util):
49 49 repo = user_util.create_repo()
50 50 repo_name = repo.repo_name
51 51 user = user_util.create_user()
52 52 user_id = user.user_id
53 53 username = user.username
54 54
55 55 # grant new
56 56 form_data = permission_update_data_generator(
57 57 csrf_token,
58 58 default='repository.write',
59 59 grant=[(user_id, 'repository.write', username, 'user')])
60 60
61 61 response = self.app.post(
62 62 route_path('edit_repo_perms',
63 63 repo_name=repo_name), form_data).follow()
64 64
65 65 assert 'Repository access permissions updated' in response
66 66
67 67 # revoke given
68 68 form_data = permission_update_data_generator(
69 69 csrf_token,
70 70 default='repository.read',
71 71 revoke=[(user_id, 'user')])
72 72
73 73 response = self.app.post(
74 74 route_path('edit_repo_perms',
75 75 repo_name=repo_name), form_data).follow()
76 76
77 77 assert 'Repository access permissions updated' in response
@@ -1,1680 +1,1680 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
26 26 from rhodecode.lib.vcs.nodes import FileNode
27 27 from rhodecode.lib.ext_json import json
28 28 from rhodecode.model.changeset_status import ChangesetStatusModel
29 29 from rhodecode.model.db import (
30 30 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
31 31 from rhodecode.model.meta import Session
32 32 from rhodecode.model.pull_request import PullRequestModel
33 33 from rhodecode.model.user import UserModel
34 34 from rhodecode.model.comment import CommentsModel
35 35 from rhodecode.tests import (
36 36 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
37 37
38 38
39 39 def route_path(name, params=None, **kwargs):
40 import urllib
40 import urllib.request, urllib.parse, urllib.error
41 41
42 42 base_url = {
43 43 'repo_changelog': '/{repo_name}/changelog',
44 44 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
45 45 'repo_commits': '/{repo_name}/commits',
46 46 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
47 47 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
48 48 'pullrequest_show_all': '/{repo_name}/pull-request',
49 49 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
50 50 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
51 51 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
52 52 'pullrequest_new': '/{repo_name}/pull-request/new',
53 53 'pullrequest_create': '/{repo_name}/pull-request/create',
54 54 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
55 55 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
56 56 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
57 57 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
58 58 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
59 59 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
60 60 }[name].format(**kwargs)
61 61
62 62 if params:
63 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
64 64 return base_url
65 65
66 66
67 67 @pytest.mark.usefixtures('app', 'autologin_user')
68 68 @pytest.mark.backends("git", "hg")
69 69 class TestPullrequestsView(object):
70 70
71 71 def test_index(self, backend):
72 72 self.app.get(route_path(
73 73 'pullrequest_new',
74 74 repo_name=backend.repo_name))
75 75
76 76 def test_option_menu_create_pull_request_exists(self, backend):
77 77 repo_name = backend.repo_name
78 78 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
79 79
80 80 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
81 81 'pullrequest_new', repo_name=repo_name)
82 82 response.mustcontain(create_pr_link)
83 83
84 84 def test_create_pr_form_with_raw_commit_id(self, backend):
85 85 repo = backend.repo
86 86
87 87 self.app.get(
88 88 route_path('pullrequest_new', repo_name=repo.repo_name,
89 89 commit=repo.get_commit().raw_id),
90 90 status=200)
91 91
92 92 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
93 93 @pytest.mark.parametrize('range_diff', ["0", "1"])
94 94 def test_show(self, pr_util, pr_merge_enabled, range_diff):
95 95 pull_request = pr_util.create_pull_request(
96 96 mergeable=pr_merge_enabled, enable_notifications=False)
97 97
98 98 response = self.app.get(route_path(
99 99 'pullrequest_show',
100 100 repo_name=pull_request.target_repo.scm_instance().name,
101 101 pull_request_id=pull_request.pull_request_id,
102 102 params={'range-diff': range_diff}))
103 103
104 104 for commit_id in pull_request.revisions:
105 105 response.mustcontain(commit_id)
106 106
107 107 response.mustcontain(pull_request.target_ref_parts.type)
108 108 response.mustcontain(pull_request.target_ref_parts.name)
109 109
110 110 response.mustcontain('class="pull-request-merge"')
111 111
112 112 if pr_merge_enabled:
113 113 response.mustcontain('Pull request reviewer approval is pending')
114 114 else:
115 115 response.mustcontain('Server-side pull request merging is disabled.')
116 116
117 117 if range_diff == "1":
118 118 response.mustcontain('Turn off: Show the diff as commit range')
119 119
120 120 def test_show_versions_of_pr(self, backend, csrf_token):
121 121 commits = [
122 122 {'message': 'initial-commit',
123 123 'added': [FileNode('test-file.txt', 'LINE1\n')]},
124 124
125 125 {'message': 'commit-1',
126 126 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
127 127 # Above is the initial version of PR that changes a single line
128 128
129 129 # from now on we'll add 3x commit adding a nother line on each step
130 130 {'message': 'commit-2',
131 131 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
132 132
133 133 {'message': 'commit-3',
134 134 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
135 135
136 136 {'message': 'commit-4',
137 137 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
138 138 ]
139 139
140 140 commit_ids = backend.create_master_repo(commits)
141 141 target = backend.create_repo(heads=['initial-commit'])
142 142 source = backend.create_repo(heads=['commit-1'])
143 143 source_repo_name = source.repo_name
144 144 target_repo_name = target.repo_name
145 145
146 146 target_ref = 'branch:{branch}:{commit_id}'.format(
147 147 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
148 148 source_ref = 'branch:{branch}:{commit_id}'.format(
149 149 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
150 150
151 151 response = self.app.post(
152 152 route_path('pullrequest_create', repo_name=source.repo_name),
153 153 [
154 154 ('source_repo', source_repo_name),
155 155 ('source_ref', source_ref),
156 156 ('target_repo', target_repo_name),
157 157 ('target_ref', target_ref),
158 158 ('common_ancestor', commit_ids['initial-commit']),
159 159 ('pullrequest_title', 'Title'),
160 160 ('pullrequest_desc', 'Description'),
161 161 ('description_renderer', 'markdown'),
162 162 ('__start__', 'review_members:sequence'),
163 163 ('__start__', 'reviewer:mapping'),
164 164 ('user_id', '1'),
165 165 ('__start__', 'reasons:sequence'),
166 166 ('reason', 'Some reason'),
167 167 ('__end__', 'reasons:sequence'),
168 168 ('__start__', 'rules:sequence'),
169 169 ('__end__', 'rules:sequence'),
170 170 ('mandatory', 'False'),
171 171 ('__end__', 'reviewer:mapping'),
172 172 ('__end__', 'review_members:sequence'),
173 173 ('__start__', 'revisions:sequence'),
174 174 ('revisions', commit_ids['commit-1']),
175 175 ('__end__', 'revisions:sequence'),
176 176 ('user', ''),
177 177 ('csrf_token', csrf_token),
178 178 ],
179 179 status=302)
180 180
181 181 location = response.headers['Location']
182 182
183 183 pull_request_id = location.rsplit('/', 1)[1]
184 184 assert pull_request_id != 'new'
185 185 pull_request = PullRequest.get(int(pull_request_id))
186 186
187 187 pull_request_id = pull_request.pull_request_id
188 188
189 189 # Show initial version of PR
190 190 response = self.app.get(
191 191 route_path('pullrequest_show',
192 192 repo_name=target_repo_name,
193 193 pull_request_id=pull_request_id))
194 194
195 195 response.mustcontain('commit-1')
196 196 response.mustcontain(no=['commit-2'])
197 197 response.mustcontain(no=['commit-3'])
198 198 response.mustcontain(no=['commit-4'])
199 199
200 200 response.mustcontain('cb-addition"></span><span>LINE2</span>')
201 201 response.mustcontain(no=['LINE3'])
202 202 response.mustcontain(no=['LINE4'])
203 203 response.mustcontain(no=['LINE5'])
204 204
205 205 # update PR #1
206 206 source_repo = Repository.get_by_repo_name(source_repo_name)
207 207 backend.pull_heads(source_repo, heads=['commit-2'])
208 208 response = self.app.post(
209 209 route_path('pullrequest_update',
210 210 repo_name=target_repo_name, pull_request_id=pull_request_id),
211 211 params={'update_commits': 'true', 'csrf_token': csrf_token})
212 212
213 213 # update PR #2
214 214 source_repo = Repository.get_by_repo_name(source_repo_name)
215 215 backend.pull_heads(source_repo, heads=['commit-3'])
216 216 response = self.app.post(
217 217 route_path('pullrequest_update',
218 218 repo_name=target_repo_name, pull_request_id=pull_request_id),
219 219 params={'update_commits': 'true', 'csrf_token': csrf_token})
220 220
221 221 # update PR #3
222 222 source_repo = Repository.get_by_repo_name(source_repo_name)
223 223 backend.pull_heads(source_repo, heads=['commit-4'])
224 224 response = self.app.post(
225 225 route_path('pullrequest_update',
226 226 repo_name=target_repo_name, pull_request_id=pull_request_id),
227 227 params={'update_commits': 'true', 'csrf_token': csrf_token})
228 228
229 229 # Show final version !
230 230 response = self.app.get(
231 231 route_path('pullrequest_show',
232 232 repo_name=target_repo_name,
233 233 pull_request_id=pull_request_id))
234 234
235 235 # 3 updates, and the latest == 4
236 236 response.mustcontain('4 versions available for this pull request')
237 237 response.mustcontain(no=['rhodecode diff rendering error'])
238 238
239 239 # initial show must have 3 commits, and 3 adds
240 240 response.mustcontain('commit-1')
241 241 response.mustcontain('commit-2')
242 242 response.mustcontain('commit-3')
243 243 response.mustcontain('commit-4')
244 244
245 245 response.mustcontain('cb-addition"></span><span>LINE2</span>')
246 246 response.mustcontain('cb-addition"></span><span>LINE3</span>')
247 247 response.mustcontain('cb-addition"></span><span>LINE4</span>')
248 248 response.mustcontain('cb-addition"></span><span>LINE5</span>')
249 249
250 250 # fetch versions
251 251 pr = PullRequest.get(pull_request_id)
252 252 versions = [x.pull_request_version_id for x in pr.versions.all()]
253 253 assert len(versions) == 3
254 254
255 255 # show v1,v2,v3,v4
256 256 def cb_line(text):
257 257 return 'cb-addition"></span><span>{}</span>'.format(text)
258 258
259 259 def cb_context(text):
260 260 return '<span class="cb-code"><span class="cb-action cb-context">' \
261 261 '</span><span>{}</span></span>'.format(text)
262 262
263 263 commit_tests = {
264 264 # in response, not in response
265 265 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
266 266 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
267 267 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
268 268 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
269 269 }
270 270 diff_tests = {
271 271 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
272 272 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
273 273 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
274 274 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
275 275 }
276 276 for idx, ver in enumerate(versions, 1):
277 277
278 278 response = self.app.get(
279 279 route_path('pullrequest_show',
280 280 repo_name=target_repo_name,
281 281 pull_request_id=pull_request_id,
282 282 params={'version': ver}))
283 283
284 284 response.mustcontain(no=['rhodecode diff rendering error'])
285 285 response.mustcontain('Showing changes at v{}'.format(idx))
286 286
287 287 yes, no = commit_tests[idx]
288 288 for y in yes:
289 289 response.mustcontain(y)
290 290 for n in no:
291 291 response.mustcontain(no=n)
292 292
293 293 yes, no = diff_tests[idx]
294 294 for y in yes:
295 295 response.mustcontain(cb_line(y))
296 296 for n in no:
297 297 response.mustcontain(no=n)
298 298
299 299 # show diff between versions
300 300 diff_compare_tests = {
301 301 1: (['LINE3'], ['LINE1', 'LINE2']),
302 302 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
303 303 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
304 304 }
305 305 for idx, ver in enumerate(versions, 1):
306 306 adds, context = diff_compare_tests[idx]
307 307
308 308 to_ver = ver+1
309 309 if idx == 3:
310 310 to_ver = 'latest'
311 311
312 312 response = self.app.get(
313 313 route_path('pullrequest_show',
314 314 repo_name=target_repo_name,
315 315 pull_request_id=pull_request_id,
316 316 params={'from_version': versions[0], 'version': to_ver}))
317 317
318 318 response.mustcontain(no=['rhodecode diff rendering error'])
319 319
320 320 for a in adds:
321 321 response.mustcontain(cb_line(a))
322 322 for c in context:
323 323 response.mustcontain(cb_context(c))
324 324
325 325 # test version v2 -> v3
326 326 response = self.app.get(
327 327 route_path('pullrequest_show',
328 328 repo_name=target_repo_name,
329 329 pull_request_id=pull_request_id,
330 330 params={'from_version': versions[1], 'version': versions[2]}))
331 331
332 332 response.mustcontain(cb_context('LINE1'))
333 333 response.mustcontain(cb_context('LINE2'))
334 334 response.mustcontain(cb_context('LINE3'))
335 335 response.mustcontain(cb_line('LINE4'))
336 336
337 337 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
338 338 # Logout
339 339 response = self.app.post(
340 340 h.route_path('logout'),
341 341 params={'csrf_token': csrf_token})
342 342 # Login as regular user
343 343 response = self.app.post(h.route_path('login'),
344 344 {'username': TEST_USER_REGULAR_LOGIN,
345 345 'password': 'test12'})
346 346
347 347 pull_request = pr_util.create_pull_request(
348 348 author=TEST_USER_REGULAR_LOGIN)
349 349
350 350 response = self.app.get(route_path(
351 351 'pullrequest_show',
352 352 repo_name=pull_request.target_repo.scm_instance().name,
353 353 pull_request_id=pull_request.pull_request_id))
354 354
355 355 response.mustcontain('Server-side pull request merging is disabled.')
356 356
357 357 assert_response = response.assert_response()
358 358 # for regular user without a merge permissions, we don't see it
359 359 assert_response.no_element_exists('#close-pull-request-action')
360 360
361 361 user_util.grant_user_permission_to_repo(
362 362 pull_request.target_repo,
363 363 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
364 364 'repository.write')
365 365 response = self.app.get(route_path(
366 366 'pullrequest_show',
367 367 repo_name=pull_request.target_repo.scm_instance().name,
368 368 pull_request_id=pull_request.pull_request_id))
369 369
370 370 response.mustcontain('Server-side pull request merging is disabled.')
371 371
372 372 assert_response = response.assert_response()
373 373 # now regular user has a merge permissions, we have CLOSE button
374 374 assert_response.one_element_exists('#close-pull-request-action')
375 375
376 376 def test_show_invalid_commit_id(self, pr_util):
377 377 # Simulating invalid revisions which will cause a lookup error
378 378 pull_request = pr_util.create_pull_request()
379 379 pull_request.revisions = ['invalid']
380 380 Session().add(pull_request)
381 381 Session().commit()
382 382
383 383 response = self.app.get(route_path(
384 384 'pullrequest_show',
385 385 repo_name=pull_request.target_repo.scm_instance().name,
386 386 pull_request_id=pull_request.pull_request_id))
387 387
388 388 for commit_id in pull_request.revisions:
389 389 response.mustcontain(commit_id)
390 390
391 391 def test_show_invalid_source_reference(self, pr_util):
392 392 pull_request = pr_util.create_pull_request()
393 393 pull_request.source_ref = 'branch:b:invalid'
394 394 Session().add(pull_request)
395 395 Session().commit()
396 396
397 397 self.app.get(route_path(
398 398 'pullrequest_show',
399 399 repo_name=pull_request.target_repo.scm_instance().name,
400 400 pull_request_id=pull_request.pull_request_id))
401 401
402 402 def test_edit_title_description(self, pr_util, csrf_token):
403 403 pull_request = pr_util.create_pull_request()
404 404 pull_request_id = pull_request.pull_request_id
405 405
406 406 response = self.app.post(
407 407 route_path('pullrequest_update',
408 408 repo_name=pull_request.target_repo.repo_name,
409 409 pull_request_id=pull_request_id),
410 410 params={
411 411 'edit_pull_request': 'true',
412 412 'title': 'New title',
413 413 'description': 'New description',
414 414 'csrf_token': csrf_token})
415 415
416 416 assert_session_flash(
417 417 response, u'Pull request title & description updated.',
418 418 category='success')
419 419
420 420 pull_request = PullRequest.get(pull_request_id)
421 421 assert pull_request.title == 'New title'
422 422 assert pull_request.description == 'New description'
423 423
424 424 def test_edit_title_description(self, pr_util, csrf_token):
425 425 pull_request = pr_util.create_pull_request()
426 426 pull_request_id = pull_request.pull_request_id
427 427
428 428 response = self.app.post(
429 429 route_path('pullrequest_update',
430 430 repo_name=pull_request.target_repo.repo_name,
431 431 pull_request_id=pull_request_id),
432 432 params={
433 433 'edit_pull_request': 'true',
434 434 'title': 'New title {} {2} {foo}',
435 435 'description': 'New description',
436 436 'csrf_token': csrf_token})
437 437
438 438 assert_session_flash(
439 439 response, u'Pull request title & description updated.',
440 440 category='success')
441 441
442 442 pull_request = PullRequest.get(pull_request_id)
443 443 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
444 444
445 445 def test_edit_title_description_closed(self, pr_util, csrf_token):
446 446 pull_request = pr_util.create_pull_request()
447 447 pull_request_id = pull_request.pull_request_id
448 448 repo_name = pull_request.target_repo.repo_name
449 449 pr_util.close()
450 450
451 451 response = self.app.post(
452 452 route_path('pullrequest_update',
453 453 repo_name=repo_name, pull_request_id=pull_request_id),
454 454 params={
455 455 'edit_pull_request': 'true',
456 456 'title': 'New title',
457 457 'description': 'New description',
458 458 'csrf_token': csrf_token}, status=200)
459 459 assert_session_flash(
460 460 response, u'Cannot update closed pull requests.',
461 461 category='error')
462 462
463 463 def test_update_invalid_source_reference(self, pr_util, csrf_token):
464 464 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
465 465
466 466 pull_request = pr_util.create_pull_request()
467 467 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
468 468 Session().add(pull_request)
469 469 Session().commit()
470 470
471 471 pull_request_id = pull_request.pull_request_id
472 472
473 473 response = self.app.post(
474 474 route_path('pullrequest_update',
475 475 repo_name=pull_request.target_repo.repo_name,
476 476 pull_request_id=pull_request_id),
477 477 params={'update_commits': 'true', 'csrf_token': csrf_token})
478 478
479 479 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
480 480 UpdateFailureReason.MISSING_SOURCE_REF])
481 481 assert_session_flash(response, expected_msg, category='error')
482 482
483 483 def test_missing_target_reference(self, pr_util, csrf_token):
484 484 from rhodecode.lib.vcs.backends.base import MergeFailureReason
485 485 pull_request = pr_util.create_pull_request(
486 486 approved=True, mergeable=True)
487 487 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
488 488 pull_request.target_ref = unicode_reference
489 489 Session().add(pull_request)
490 490 Session().commit()
491 491
492 492 pull_request_id = pull_request.pull_request_id
493 493 pull_request_url = route_path(
494 494 'pullrequest_show',
495 495 repo_name=pull_request.target_repo.repo_name,
496 496 pull_request_id=pull_request_id)
497 497
498 498 response = self.app.get(pull_request_url)
499 499 target_ref_id = 'invalid-branch'
500 500 merge_resp = MergeResponse(
501 501 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
502 502 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
503 503 response.assert_response().element_contains(
504 504 'div[data-role="merge-message"]', merge_resp.merge_status_message)
505 505
506 506 def test_comment_and_close_pull_request_custom_message_approved(
507 507 self, pr_util, csrf_token, xhr_header):
508 508
509 509 pull_request = pr_util.create_pull_request(approved=True)
510 510 pull_request_id = pull_request.pull_request_id
511 511 author = pull_request.user_id
512 512 repo = pull_request.target_repo.repo_id
513 513
514 514 self.app.post(
515 515 route_path('pullrequest_comment_create',
516 516 repo_name=pull_request.target_repo.scm_instance().name,
517 517 pull_request_id=pull_request_id),
518 518 params={
519 519 'close_pull_request': '1',
520 520 'text': 'Closing a PR',
521 521 'csrf_token': csrf_token},
522 522 extra_environ=xhr_header,)
523 523
524 524 journal = UserLog.query()\
525 525 .filter(UserLog.user_id == author)\
526 526 .filter(UserLog.repository_id == repo) \
527 527 .order_by(UserLog.user_log_id.asc()) \
528 528 .all()
529 529 assert journal[-1].action == 'repo.pull_request.close'
530 530
531 531 pull_request = PullRequest.get(pull_request_id)
532 532 assert pull_request.is_closed()
533 533
534 534 status = ChangesetStatusModel().get_status(
535 535 pull_request.source_repo, pull_request=pull_request)
536 536 assert status == ChangesetStatus.STATUS_APPROVED
537 537 comments = ChangesetComment().query() \
538 538 .filter(ChangesetComment.pull_request == pull_request) \
539 539 .order_by(ChangesetComment.comment_id.asc())\
540 540 .all()
541 541 assert comments[-1].text == 'Closing a PR'
542 542
543 543 def test_comment_force_close_pull_request_rejected(
544 544 self, pr_util, csrf_token, xhr_header):
545 545 pull_request = pr_util.create_pull_request()
546 546 pull_request_id = pull_request.pull_request_id
547 547 PullRequestModel().update_reviewers(
548 548 pull_request_id, [
549 549 (1, ['reason'], False, 'reviewer', []),
550 550 (2, ['reason2'], False, 'reviewer', [])],
551 551 pull_request.author)
552 552 author = pull_request.user_id
553 553 repo = pull_request.target_repo.repo_id
554 554
555 555 self.app.post(
556 556 route_path('pullrequest_comment_create',
557 557 repo_name=pull_request.target_repo.scm_instance().name,
558 558 pull_request_id=pull_request_id),
559 559 params={
560 560 'close_pull_request': '1',
561 561 'csrf_token': csrf_token},
562 562 extra_environ=xhr_header)
563 563
564 564 pull_request = PullRequest.get(pull_request_id)
565 565
566 566 journal = UserLog.query()\
567 567 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
568 568 .order_by(UserLog.user_log_id.asc()) \
569 569 .all()
570 570 assert journal[-1].action == 'repo.pull_request.close'
571 571
572 572 # check only the latest status, not the review status
573 573 status = ChangesetStatusModel().get_status(
574 574 pull_request.source_repo, pull_request=pull_request)
575 575 assert status == ChangesetStatus.STATUS_REJECTED
576 576
577 577 def test_comment_and_close_pull_request(
578 578 self, pr_util, csrf_token, xhr_header):
579 579 pull_request = pr_util.create_pull_request()
580 580 pull_request_id = pull_request.pull_request_id
581 581
582 582 response = self.app.post(
583 583 route_path('pullrequest_comment_create',
584 584 repo_name=pull_request.target_repo.scm_instance().name,
585 585 pull_request_id=pull_request.pull_request_id),
586 586 params={
587 587 'close_pull_request': 'true',
588 588 'csrf_token': csrf_token},
589 589 extra_environ=xhr_header)
590 590
591 591 assert response.json
592 592
593 593 pull_request = PullRequest.get(pull_request_id)
594 594 assert pull_request.is_closed()
595 595
596 596 # check only the latest status, not the review status
597 597 status = ChangesetStatusModel().get_status(
598 598 pull_request.source_repo, pull_request=pull_request)
599 599 assert status == ChangesetStatus.STATUS_REJECTED
600 600
601 601 def test_comment_and_close_pull_request_try_edit_comment(
602 602 self, pr_util, csrf_token, xhr_header
603 603 ):
604 604 pull_request = pr_util.create_pull_request()
605 605 pull_request_id = pull_request.pull_request_id
606 606 target_scm = pull_request.target_repo.scm_instance()
607 607 target_scm_name = target_scm.name
608 608
609 609 response = self.app.post(
610 610 route_path(
611 611 'pullrequest_comment_create',
612 612 repo_name=target_scm_name,
613 613 pull_request_id=pull_request_id,
614 614 ),
615 615 params={
616 616 'close_pull_request': 'true',
617 617 'csrf_token': csrf_token,
618 618 },
619 619 extra_environ=xhr_header)
620 620
621 621 assert response.json
622 622
623 623 pull_request = PullRequest.get(pull_request_id)
624 624 target_scm = pull_request.target_repo.scm_instance()
625 625 target_scm_name = target_scm.name
626 626 assert pull_request.is_closed()
627 627
628 628 # check only the latest status, not the review status
629 629 status = ChangesetStatusModel().get_status(
630 630 pull_request.source_repo, pull_request=pull_request)
631 631 assert status == ChangesetStatus.STATUS_REJECTED
632 632
633 633 for comment_id in response.json.keys():
634 634 test_text = 'test'
635 635 response = self.app.post(
636 636 route_path(
637 637 'pullrequest_comment_edit',
638 638 repo_name=target_scm_name,
639 639 pull_request_id=pull_request_id,
640 640 comment_id=comment_id,
641 641 ),
642 642 extra_environ=xhr_header,
643 643 params={
644 644 'csrf_token': csrf_token,
645 645 'text': test_text,
646 646 },
647 647 status=403,
648 648 )
649 649 assert response.status_int == 403
650 650
651 651 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
652 652 pull_request = pr_util.create_pull_request()
653 653 target_scm = pull_request.target_repo.scm_instance()
654 654 target_scm_name = target_scm.name
655 655
656 656 response = self.app.post(
657 657 route_path(
658 658 'pullrequest_comment_create',
659 659 repo_name=target_scm_name,
660 660 pull_request_id=pull_request.pull_request_id),
661 661 params={
662 662 'csrf_token': csrf_token,
663 663 'text': 'init',
664 664 },
665 665 extra_environ=xhr_header,
666 666 )
667 667 assert response.json
668 668
669 669 for comment_id in response.json.keys():
670 670 assert comment_id
671 671 test_text = 'test'
672 672 self.app.post(
673 673 route_path(
674 674 'pullrequest_comment_edit',
675 675 repo_name=target_scm_name,
676 676 pull_request_id=pull_request.pull_request_id,
677 677 comment_id=comment_id,
678 678 ),
679 679 extra_environ=xhr_header,
680 680 params={
681 681 'csrf_token': csrf_token,
682 682 'text': test_text,
683 683 'version': '0',
684 684 },
685 685
686 686 )
687 687 text_form_db = ChangesetComment.query().filter(
688 688 ChangesetComment.comment_id == comment_id).first().text
689 689 assert test_text == text_form_db
690 690
691 691 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
692 692 pull_request = pr_util.create_pull_request()
693 693 target_scm = pull_request.target_repo.scm_instance()
694 694 target_scm_name = target_scm.name
695 695
696 696 response = self.app.post(
697 697 route_path(
698 698 'pullrequest_comment_create',
699 699 repo_name=target_scm_name,
700 700 pull_request_id=pull_request.pull_request_id),
701 701 params={
702 702 'csrf_token': csrf_token,
703 703 'text': 'init',
704 704 },
705 705 extra_environ=xhr_header,
706 706 )
707 707 assert response.json
708 708
709 709 for comment_id in response.json.keys():
710 710 test_text = 'init'
711 711 response = self.app.post(
712 712 route_path(
713 713 'pullrequest_comment_edit',
714 714 repo_name=target_scm_name,
715 715 pull_request_id=pull_request.pull_request_id,
716 716 comment_id=comment_id,
717 717 ),
718 718 extra_environ=xhr_header,
719 719 params={
720 720 'csrf_token': csrf_token,
721 721 'text': test_text,
722 722 'version': '0',
723 723 },
724 724 status=404,
725 725
726 726 )
727 727 assert response.status_int == 404
728 728
729 729 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
730 730 pull_request = pr_util.create_pull_request()
731 731 target_scm = pull_request.target_repo.scm_instance()
732 732 target_scm_name = target_scm.name
733 733
734 734 response = self.app.post(
735 735 route_path(
736 736 'pullrequest_comment_create',
737 737 repo_name=target_scm_name,
738 738 pull_request_id=pull_request.pull_request_id),
739 739 params={
740 740 'csrf_token': csrf_token,
741 741 'text': 'init',
742 742 },
743 743 extra_environ=xhr_header,
744 744 )
745 745 assert response.json
746 746 for comment_id in response.json.keys():
747 747 test_text = 'test'
748 748 self.app.post(
749 749 route_path(
750 750 'pullrequest_comment_edit',
751 751 repo_name=target_scm_name,
752 752 pull_request_id=pull_request.pull_request_id,
753 753 comment_id=comment_id,
754 754 ),
755 755 extra_environ=xhr_header,
756 756 params={
757 757 'csrf_token': csrf_token,
758 758 'text': test_text,
759 759 'version': '0',
760 760 },
761 761
762 762 )
763 763 test_text_v2 = 'test_v2'
764 764 response = self.app.post(
765 765 route_path(
766 766 'pullrequest_comment_edit',
767 767 repo_name=target_scm_name,
768 768 pull_request_id=pull_request.pull_request_id,
769 769 comment_id=comment_id,
770 770 ),
771 771 extra_environ=xhr_header,
772 772 params={
773 773 'csrf_token': csrf_token,
774 774 'text': test_text_v2,
775 775 'version': '0',
776 776 },
777 777 status=409,
778 778 )
779 779 assert response.status_int == 409
780 780
781 781 text_form_db = ChangesetComment.query().filter(
782 782 ChangesetComment.comment_id == comment_id).first().text
783 783
784 784 assert test_text == text_form_db
785 785 assert test_text_v2 != text_form_db
786 786
787 787 def test_comment_and_comment_edit_permissions_forbidden(
788 788 self, autologin_regular_user, user_regular, user_admin, pr_util,
789 789 csrf_token, xhr_header):
790 790 pull_request = pr_util.create_pull_request(
791 791 author=user_admin.username, enable_notifications=False)
792 792 comment = CommentsModel().create(
793 793 text='test',
794 794 repo=pull_request.target_repo.scm_instance().name,
795 795 user=user_admin,
796 796 pull_request=pull_request,
797 797 )
798 798 response = self.app.post(
799 799 route_path(
800 800 'pullrequest_comment_edit',
801 801 repo_name=pull_request.target_repo.scm_instance().name,
802 802 pull_request_id=pull_request.pull_request_id,
803 803 comment_id=comment.comment_id,
804 804 ),
805 805 extra_environ=xhr_header,
806 806 params={
807 807 'csrf_token': csrf_token,
808 808 'text': 'test_text',
809 809 },
810 810 status=403,
811 811 )
812 812 assert response.status_int == 403
813 813
814 814 def test_create_pull_request(self, backend, csrf_token):
815 815 commits = [
816 816 {'message': 'ancestor'},
817 817 {'message': 'change'},
818 818 {'message': 'change2'},
819 819 ]
820 820 commit_ids = backend.create_master_repo(commits)
821 821 target = backend.create_repo(heads=['ancestor'])
822 822 source = backend.create_repo(heads=['change2'])
823 823
824 824 response = self.app.post(
825 825 route_path('pullrequest_create', repo_name=source.repo_name),
826 826 [
827 827 ('source_repo', source.repo_name),
828 828 ('source_ref', 'branch:default:' + commit_ids['change2']),
829 829 ('target_repo', target.repo_name),
830 830 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
831 831 ('common_ancestor', commit_ids['ancestor']),
832 832 ('pullrequest_title', 'Title'),
833 833 ('pullrequest_desc', 'Description'),
834 834 ('description_renderer', 'markdown'),
835 835 ('__start__', 'review_members:sequence'),
836 836 ('__start__', 'reviewer:mapping'),
837 837 ('user_id', '1'),
838 838 ('__start__', 'reasons:sequence'),
839 839 ('reason', 'Some reason'),
840 840 ('__end__', 'reasons:sequence'),
841 841 ('__start__', 'rules:sequence'),
842 842 ('__end__', 'rules:sequence'),
843 843 ('mandatory', 'False'),
844 844 ('__end__', 'reviewer:mapping'),
845 845 ('__end__', 'review_members:sequence'),
846 846 ('__start__', 'revisions:sequence'),
847 847 ('revisions', commit_ids['change']),
848 848 ('revisions', commit_ids['change2']),
849 849 ('__end__', 'revisions:sequence'),
850 850 ('user', ''),
851 851 ('csrf_token', csrf_token),
852 852 ],
853 853 status=302)
854 854
855 855 location = response.headers['Location']
856 856 pull_request_id = location.rsplit('/', 1)[1]
857 857 assert pull_request_id != 'new'
858 858 pull_request = PullRequest.get(int(pull_request_id))
859 859
860 860 # check that we have now both revisions
861 861 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
862 862 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
863 863 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
864 864 assert pull_request.target_ref == expected_target_ref
865 865
866 866 def test_reviewer_notifications(self, backend, csrf_token):
867 867 # We have to use the app.post for this test so it will create the
868 868 # notifications properly with the new PR
869 869 commits = [
870 870 {'message': 'ancestor',
871 871 'added': [FileNode('file_A', content='content_of_ancestor')]},
872 872 {'message': 'change',
873 873 'added': [FileNode('file_a', content='content_of_change')]},
874 874 {'message': 'change-child'},
875 875 {'message': 'ancestor-child', 'parents': ['ancestor'],
876 876 'added': [
877 877 FileNode('file_B', content='content_of_ancestor_child')]},
878 878 {'message': 'ancestor-child-2'},
879 879 ]
880 880 commit_ids = backend.create_master_repo(commits)
881 881 target = backend.create_repo(heads=['ancestor-child'])
882 882 source = backend.create_repo(heads=['change'])
883 883
884 884 response = self.app.post(
885 885 route_path('pullrequest_create', repo_name=source.repo_name),
886 886 [
887 887 ('source_repo', source.repo_name),
888 888 ('source_ref', 'branch:default:' + commit_ids['change']),
889 889 ('target_repo', target.repo_name),
890 890 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
891 891 ('common_ancestor', commit_ids['ancestor']),
892 892 ('pullrequest_title', 'Title'),
893 893 ('pullrequest_desc', 'Description'),
894 894 ('description_renderer', 'markdown'),
895 895 ('__start__', 'review_members:sequence'),
896 896 ('__start__', 'reviewer:mapping'),
897 897 ('user_id', '2'),
898 898 ('__start__', 'reasons:sequence'),
899 899 ('reason', 'Some reason'),
900 900 ('__end__', 'reasons:sequence'),
901 901 ('__start__', 'rules:sequence'),
902 902 ('__end__', 'rules:sequence'),
903 903 ('mandatory', 'False'),
904 904 ('__end__', 'reviewer:mapping'),
905 905 ('__end__', 'review_members:sequence'),
906 906 ('__start__', 'revisions:sequence'),
907 907 ('revisions', commit_ids['change']),
908 908 ('__end__', 'revisions:sequence'),
909 909 ('user', ''),
910 910 ('csrf_token', csrf_token),
911 911 ],
912 912 status=302)
913 913
914 914 location = response.headers['Location']
915 915
916 916 pull_request_id = location.rsplit('/', 1)[1]
917 917 assert pull_request_id != 'new'
918 918 pull_request = PullRequest.get(int(pull_request_id))
919 919
920 920 # Check that a notification was made
921 921 notifications = Notification.query()\
922 922 .filter(Notification.created_by == pull_request.author.user_id,
923 923 Notification.type_ == Notification.TYPE_PULL_REQUEST,
924 924 Notification.subject.contains(
925 925 "requested a pull request review. !%s" % pull_request_id))
926 926 assert len(notifications.all()) == 1
927 927
928 928 # Change reviewers and check that a notification was made
929 929 PullRequestModel().update_reviewers(
930 930 pull_request.pull_request_id, [
931 931 (1, [], False, 'reviewer', [])
932 932 ],
933 933 pull_request.author)
934 934 assert len(notifications.all()) == 2
935 935
936 936 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
937 937 commits = [
938 938 {'message': 'ancestor',
939 939 'added': [FileNode('file_A', content='content_of_ancestor')]},
940 940 {'message': 'change',
941 941 'added': [FileNode('file_a', content='content_of_change')]},
942 942 {'message': 'change-child'},
943 943 {'message': 'ancestor-child', 'parents': ['ancestor'],
944 944 'added': [
945 945 FileNode('file_B', content='content_of_ancestor_child')]},
946 946 {'message': 'ancestor-child-2'},
947 947 ]
948 948 commit_ids = backend.create_master_repo(commits)
949 949 target = backend.create_repo(heads=['ancestor-child'])
950 950 source = backend.create_repo(heads=['change'])
951 951
952 952 response = self.app.post(
953 953 route_path('pullrequest_create', repo_name=source.repo_name),
954 954 [
955 955 ('source_repo', source.repo_name),
956 956 ('source_ref', 'branch:default:' + commit_ids['change']),
957 957 ('target_repo', target.repo_name),
958 958 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
959 959 ('common_ancestor', commit_ids['ancestor']),
960 960 ('pullrequest_title', 'Title'),
961 961 ('pullrequest_desc', 'Description'),
962 962 ('description_renderer', 'markdown'),
963 963 ('__start__', 'review_members:sequence'),
964 964 ('__start__', 'reviewer:mapping'),
965 965 ('user_id', '1'),
966 966 ('__start__', 'reasons:sequence'),
967 967 ('reason', 'Some reason'),
968 968 ('__end__', 'reasons:sequence'),
969 969 ('__start__', 'rules:sequence'),
970 970 ('__end__', 'rules:sequence'),
971 971 ('mandatory', 'False'),
972 972 ('__end__', 'reviewer:mapping'),
973 973 ('__end__', 'review_members:sequence'),
974 974 ('__start__', 'revisions:sequence'),
975 975 ('revisions', commit_ids['change']),
976 976 ('__end__', 'revisions:sequence'),
977 977 ('user', ''),
978 978 ('csrf_token', csrf_token),
979 979 ],
980 980 status=302)
981 981
982 982 location = response.headers['Location']
983 983
984 984 pull_request_id = location.rsplit('/', 1)[1]
985 985 assert pull_request_id != 'new'
986 986 pull_request = PullRequest.get(int(pull_request_id))
987 987
988 988 # target_ref has to point to the ancestor's commit_id in order to
989 989 # show the correct diff
990 990 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
991 991 assert pull_request.target_ref == expected_target_ref
992 992
993 993 # Check generated diff contents
994 994 response = response.follow()
995 995 response.mustcontain(no=['content_of_ancestor'])
996 996 response.mustcontain(no=['content_of_ancestor-child'])
997 997 response.mustcontain('content_of_change')
998 998
999 999 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
1000 1000 # Clear any previous calls to rcextensions
1001 1001 rhodecode.EXTENSIONS.calls.clear()
1002 1002
1003 1003 pull_request = pr_util.create_pull_request(
1004 1004 approved=True, mergeable=True)
1005 1005 pull_request_id = pull_request.pull_request_id
1006 1006 repo_name = pull_request.target_repo.scm_instance().name,
1007 1007
1008 1008 url = route_path('pullrequest_merge',
1009 1009 repo_name=str(repo_name[0]),
1010 1010 pull_request_id=pull_request_id)
1011 1011 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1012 1012
1013 1013 pull_request = PullRequest.get(pull_request_id)
1014 1014
1015 1015 assert response.status_int == 200
1016 1016 assert pull_request.is_closed()
1017 1017 assert_pull_request_status(
1018 1018 pull_request, ChangesetStatus.STATUS_APPROVED)
1019 1019
1020 1020 # Check the relevant log entries were added
1021 1021 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1022 1022 actions = [log.action for log in user_logs]
1023 1023 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1024 1024 expected_actions = [
1025 1025 u'repo.pull_request.close',
1026 1026 u'repo.pull_request.merge',
1027 1027 u'repo.pull_request.comment.create'
1028 1028 ]
1029 1029 assert actions == expected_actions
1030 1030
1031 1031 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1032 1032 actions = [log for log in user_logs]
1033 1033 assert actions[-1].action == 'user.push'
1034 1034 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1035 1035
1036 1036 # Check post_push rcextension was really executed
1037 1037 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1038 1038 assert len(push_calls) == 1
1039 1039 unused_last_call_args, last_call_kwargs = push_calls[0]
1040 1040 assert last_call_kwargs['action'] == 'push'
1041 1041 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1042 1042
1043 1043 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1044 1044 pull_request = pr_util.create_pull_request(mergeable=False)
1045 1045 pull_request_id = pull_request.pull_request_id
1046 1046 pull_request = PullRequest.get(pull_request_id)
1047 1047
1048 1048 response = self.app.post(
1049 1049 route_path('pullrequest_merge',
1050 1050 repo_name=pull_request.target_repo.scm_instance().name,
1051 1051 pull_request_id=pull_request.pull_request_id),
1052 1052 params={'csrf_token': csrf_token}).follow()
1053 1053
1054 1054 assert response.status_int == 200
1055 1055 response.mustcontain(
1056 1056 'Merge is not currently possible because of below failed checks.')
1057 1057 response.mustcontain('Server-side pull request merging is disabled.')
1058 1058
1059 1059 @pytest.mark.skip_backends('svn')
1060 1060 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1061 1061 pull_request = pr_util.create_pull_request(mergeable=True)
1062 1062 pull_request_id = pull_request.pull_request_id
1063 1063 repo_name = pull_request.target_repo.scm_instance().name
1064 1064
1065 1065 response = self.app.post(
1066 1066 route_path('pullrequest_merge',
1067 1067 repo_name=repo_name, pull_request_id=pull_request_id),
1068 1068 params={'csrf_token': csrf_token}).follow()
1069 1069
1070 1070 assert response.status_int == 200
1071 1071
1072 1072 response.mustcontain(
1073 1073 'Merge is not currently possible because of below failed checks.')
1074 1074 response.mustcontain('Pull request reviewer approval is pending.')
1075 1075
1076 1076 def test_merge_pull_request_renders_failure_reason(
1077 1077 self, user_regular, csrf_token, pr_util):
1078 1078 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1079 1079 pull_request_id = pull_request.pull_request_id
1080 1080 repo_name = pull_request.target_repo.scm_instance().name
1081 1081
1082 1082 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1083 1083 MergeFailureReason.PUSH_FAILED,
1084 1084 metadata={'target': 'shadow repo',
1085 1085 'merge_commit': 'xxx'})
1086 1086 model_patcher = mock.patch.multiple(
1087 1087 PullRequestModel,
1088 1088 merge_repo=mock.Mock(return_value=merge_resp),
1089 1089 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1090 1090
1091 1091 with model_patcher:
1092 1092 response = self.app.post(
1093 1093 route_path('pullrequest_merge',
1094 1094 repo_name=repo_name,
1095 1095 pull_request_id=pull_request_id),
1096 1096 params={'csrf_token': csrf_token}, status=302)
1097 1097
1098 1098 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1099 1099 metadata={'target': 'shadow repo',
1100 1100 'merge_commit': 'xxx'})
1101 1101 assert_session_flash(response, merge_resp.merge_status_message)
1102 1102
1103 1103 def test_update_source_revision(self, backend, csrf_token):
1104 1104 commits = [
1105 1105 {'message': 'ancestor'},
1106 1106 {'message': 'change'},
1107 1107 {'message': 'change-2'},
1108 1108 ]
1109 1109 commit_ids = backend.create_master_repo(commits)
1110 1110 target = backend.create_repo(heads=['ancestor'])
1111 1111 source = backend.create_repo(heads=['change'])
1112 1112
1113 1113 # create pr from a in source to A in target
1114 1114 pull_request = PullRequest()
1115 1115
1116 1116 pull_request.source_repo = source
1117 1117 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1118 1118 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1119 1119
1120 1120 pull_request.target_repo = target
1121 1121 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1122 1122 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1123 1123
1124 1124 pull_request.revisions = [commit_ids['change']]
1125 1125 pull_request.title = u"Test"
1126 1126 pull_request.description = u"Description"
1127 1127 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1128 1128 pull_request.pull_request_state = PullRequest.STATE_CREATED
1129 1129 Session().add(pull_request)
1130 1130 Session().commit()
1131 1131 pull_request_id = pull_request.pull_request_id
1132 1132
1133 1133 # source has ancestor - change - change-2
1134 1134 backend.pull_heads(source, heads=['change-2'])
1135 1135 target_repo_name = target.repo_name
1136 1136
1137 1137 # update PR
1138 1138 self.app.post(
1139 1139 route_path('pullrequest_update',
1140 1140 repo_name=target_repo_name, pull_request_id=pull_request_id),
1141 1141 params={'update_commits': 'true', 'csrf_token': csrf_token})
1142 1142
1143 1143 response = self.app.get(
1144 1144 route_path('pullrequest_show',
1145 1145 repo_name=target_repo_name,
1146 1146 pull_request_id=pull_request.pull_request_id))
1147 1147
1148 1148 assert response.status_int == 200
1149 1149 response.mustcontain('Pull request updated to')
1150 1150 response.mustcontain('with 1 added, 0 removed commits.')
1151 1151
1152 1152 # check that we have now both revisions
1153 1153 pull_request = PullRequest.get(pull_request_id)
1154 1154 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1155 1155
1156 1156 def test_update_target_revision(self, backend, csrf_token):
1157 1157 commits = [
1158 1158 {'message': 'ancestor'},
1159 1159 {'message': 'change'},
1160 1160 {'message': 'ancestor-new', 'parents': ['ancestor']},
1161 1161 {'message': 'change-rebased'},
1162 1162 ]
1163 1163 commit_ids = backend.create_master_repo(commits)
1164 1164 target = backend.create_repo(heads=['ancestor'])
1165 1165 source = backend.create_repo(heads=['change'])
1166 1166
1167 1167 # create pr from a in source to A in target
1168 1168 pull_request = PullRequest()
1169 1169
1170 1170 pull_request.source_repo = source
1171 1171 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1172 1172 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1173 1173
1174 1174 pull_request.target_repo = target
1175 1175 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1176 1176 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1177 1177
1178 1178 pull_request.revisions = [commit_ids['change']]
1179 1179 pull_request.title = u"Test"
1180 1180 pull_request.description = u"Description"
1181 1181 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1182 1182 pull_request.pull_request_state = PullRequest.STATE_CREATED
1183 1183
1184 1184 Session().add(pull_request)
1185 1185 Session().commit()
1186 1186 pull_request_id = pull_request.pull_request_id
1187 1187
1188 1188 # target has ancestor - ancestor-new
1189 1189 # source has ancestor - ancestor-new - change-rebased
1190 1190 backend.pull_heads(target, heads=['ancestor-new'])
1191 1191 backend.pull_heads(source, heads=['change-rebased'])
1192 1192 target_repo_name = target.repo_name
1193 1193
1194 1194 # update PR
1195 1195 url = route_path('pullrequest_update',
1196 1196 repo_name=target_repo_name,
1197 1197 pull_request_id=pull_request_id)
1198 1198 self.app.post(url,
1199 1199 params={'update_commits': 'true', 'csrf_token': csrf_token},
1200 1200 status=200)
1201 1201
1202 1202 # check that we have now both revisions
1203 1203 pull_request = PullRequest.get(pull_request_id)
1204 1204 assert pull_request.revisions == [commit_ids['change-rebased']]
1205 1205 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1206 1206 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1207 1207
1208 1208 response = self.app.get(
1209 1209 route_path('pullrequest_show',
1210 1210 repo_name=target_repo_name,
1211 1211 pull_request_id=pull_request.pull_request_id))
1212 1212 assert response.status_int == 200
1213 1213 response.mustcontain('Pull request updated to')
1214 1214 response.mustcontain('with 1 added, 1 removed commits.')
1215 1215
1216 1216 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1217 1217 backend = backend_git
1218 1218 commits = [
1219 1219 {'message': 'master-commit-1'},
1220 1220 {'message': 'master-commit-2-change-1'},
1221 1221 {'message': 'master-commit-3-change-2'},
1222 1222
1223 1223 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1224 1224 {'message': 'feat-commit-2'},
1225 1225 ]
1226 1226 commit_ids = backend.create_master_repo(commits)
1227 1227 target = backend.create_repo(heads=['master-commit-3-change-2'])
1228 1228 source = backend.create_repo(heads=['feat-commit-2'])
1229 1229
1230 1230 # create pr from a in source to A in target
1231 1231 pull_request = PullRequest()
1232 1232 pull_request.source_repo = source
1233 1233
1234 1234 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1235 1235 branch=backend.default_branch_name,
1236 1236 commit_id=commit_ids['master-commit-3-change-2'])
1237 1237
1238 1238 pull_request.target_repo = target
1239 1239 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1240 1240 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1241 1241
1242 1242 pull_request.revisions = [
1243 1243 commit_ids['feat-commit-1'],
1244 1244 commit_ids['feat-commit-2']
1245 1245 ]
1246 1246 pull_request.title = u"Test"
1247 1247 pull_request.description = u"Description"
1248 1248 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1249 1249 pull_request.pull_request_state = PullRequest.STATE_CREATED
1250 1250 Session().add(pull_request)
1251 1251 Session().commit()
1252 1252 pull_request_id = pull_request.pull_request_id
1253 1253
1254 1254 # PR is created, now we simulate a force-push into target,
1255 1255 # that drops a 2 last commits
1256 1256 vcsrepo = target.scm_instance()
1257 1257 vcsrepo.config.clear_section('hooks')
1258 1258 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1259 1259 target_repo_name = target.repo_name
1260 1260
1261 1261 # update PR
1262 1262 url = route_path('pullrequest_update',
1263 1263 repo_name=target_repo_name,
1264 1264 pull_request_id=pull_request_id)
1265 1265 self.app.post(url,
1266 1266 params={'update_commits': 'true', 'csrf_token': csrf_token},
1267 1267 status=200)
1268 1268
1269 1269 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1270 1270 assert response.status_int == 200
1271 1271 response.mustcontain('Pull request updated to')
1272 1272 response.mustcontain('with 0 added, 0 removed commits.')
1273 1273
1274 1274 def test_update_of_ancestor_reference(self, backend, csrf_token):
1275 1275 commits = [
1276 1276 {'message': 'ancestor'},
1277 1277 {'message': 'change'},
1278 1278 {'message': 'change-2'},
1279 1279 {'message': 'ancestor-new', 'parents': ['ancestor']},
1280 1280 {'message': 'change-rebased'},
1281 1281 ]
1282 1282 commit_ids = backend.create_master_repo(commits)
1283 1283 target = backend.create_repo(heads=['ancestor'])
1284 1284 source = backend.create_repo(heads=['change'])
1285 1285
1286 1286 # create pr from a in source to A in target
1287 1287 pull_request = PullRequest()
1288 1288 pull_request.source_repo = source
1289 1289
1290 1290 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1291 1291 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1292 1292 pull_request.target_repo = target
1293 1293 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1294 1294 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1295 1295 pull_request.revisions = [commit_ids['change']]
1296 1296 pull_request.title = u"Test"
1297 1297 pull_request.description = u"Description"
1298 1298 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1299 1299 pull_request.pull_request_state = PullRequest.STATE_CREATED
1300 1300 Session().add(pull_request)
1301 1301 Session().commit()
1302 1302 pull_request_id = pull_request.pull_request_id
1303 1303
1304 1304 # target has ancestor - ancestor-new
1305 1305 # source has ancestor - ancestor-new - change-rebased
1306 1306 backend.pull_heads(target, heads=['ancestor-new'])
1307 1307 backend.pull_heads(source, heads=['change-rebased'])
1308 1308 target_repo_name = target.repo_name
1309 1309
1310 1310 # update PR
1311 1311 self.app.post(
1312 1312 route_path('pullrequest_update',
1313 1313 repo_name=target_repo_name, pull_request_id=pull_request_id),
1314 1314 params={'update_commits': 'true', 'csrf_token': csrf_token},
1315 1315 status=200)
1316 1316
1317 1317 # Expect the target reference to be updated correctly
1318 1318 pull_request = PullRequest.get(pull_request_id)
1319 1319 assert pull_request.revisions == [commit_ids['change-rebased']]
1320 1320 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1321 1321 branch=backend.default_branch_name,
1322 1322 commit_id=commit_ids['ancestor-new'])
1323 1323 assert pull_request.target_ref == expected_target_ref
1324 1324
1325 1325 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1326 1326 branch_name = 'development'
1327 1327 commits = [
1328 1328 {'message': 'initial-commit'},
1329 1329 {'message': 'old-feature'},
1330 1330 {'message': 'new-feature', 'branch': branch_name},
1331 1331 ]
1332 1332 repo = backend_git.create_repo(commits)
1333 1333 repo_name = repo.repo_name
1334 1334 commit_ids = backend_git.commit_ids
1335 1335
1336 1336 pull_request = PullRequest()
1337 1337 pull_request.source_repo = repo
1338 1338 pull_request.target_repo = repo
1339 1339 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1340 1340 branch=branch_name, commit_id=commit_ids['new-feature'])
1341 1341 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1342 1342 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1343 1343 pull_request.revisions = [commit_ids['new-feature']]
1344 1344 pull_request.title = u"Test"
1345 1345 pull_request.description = u"Description"
1346 1346 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1347 1347 pull_request.pull_request_state = PullRequest.STATE_CREATED
1348 1348 Session().add(pull_request)
1349 1349 Session().commit()
1350 1350
1351 1351 pull_request_id = pull_request.pull_request_id
1352 1352
1353 1353 vcs = repo.scm_instance()
1354 1354 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1355 1355 # NOTE(marcink): run GC to ensure the commits are gone
1356 1356 vcs.run_gc()
1357 1357
1358 1358 response = self.app.get(route_path(
1359 1359 'pullrequest_show',
1360 1360 repo_name=repo_name,
1361 1361 pull_request_id=pull_request_id))
1362 1362
1363 1363 assert response.status_int == 200
1364 1364
1365 1365 response.assert_response().element_contains(
1366 1366 '#changeset_compare_view_content .alert strong',
1367 1367 'Missing commits')
1368 1368 response.assert_response().element_contains(
1369 1369 '#changeset_compare_view_content .alert',
1370 1370 'This pull request cannot be displayed, because one or more'
1371 1371 ' commits no longer exist in the source repository.')
1372 1372
1373 1373 def test_strip_commits_from_pull_request(
1374 1374 self, backend, pr_util, csrf_token):
1375 1375 commits = [
1376 1376 {'message': 'initial-commit'},
1377 1377 {'message': 'old-feature'},
1378 1378 {'message': 'new-feature', 'parents': ['initial-commit']},
1379 1379 ]
1380 1380 pull_request = pr_util.create_pull_request(
1381 1381 commits, target_head='initial-commit', source_head='new-feature',
1382 1382 revisions=['new-feature'])
1383 1383
1384 1384 vcs = pr_util.source_repository.scm_instance()
1385 1385 if backend.alias == 'git':
1386 1386 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1387 1387 else:
1388 1388 vcs.strip(pr_util.commit_ids['new-feature'])
1389 1389
1390 1390 response = self.app.get(route_path(
1391 1391 'pullrequest_show',
1392 1392 repo_name=pr_util.target_repository.repo_name,
1393 1393 pull_request_id=pull_request.pull_request_id))
1394 1394
1395 1395 assert response.status_int == 200
1396 1396
1397 1397 response.assert_response().element_contains(
1398 1398 '#changeset_compare_view_content .alert strong',
1399 1399 'Missing commits')
1400 1400 response.assert_response().element_contains(
1401 1401 '#changeset_compare_view_content .alert',
1402 1402 'This pull request cannot be displayed, because one or more'
1403 1403 ' commits no longer exist in the source repository.')
1404 1404 response.assert_response().element_contains(
1405 1405 '#update_commits',
1406 1406 'Update commits')
1407 1407
1408 1408 def test_strip_commits_and_update(
1409 1409 self, backend, pr_util, csrf_token):
1410 1410 commits = [
1411 1411 {'message': 'initial-commit'},
1412 1412 {'message': 'old-feature'},
1413 1413 {'message': 'new-feature', 'parents': ['old-feature']},
1414 1414 ]
1415 1415 pull_request = pr_util.create_pull_request(
1416 1416 commits, target_head='old-feature', source_head='new-feature',
1417 1417 revisions=['new-feature'], mergeable=True)
1418 1418 pr_id = pull_request.pull_request_id
1419 1419 target_repo_name = pull_request.target_repo.repo_name
1420 1420
1421 1421 vcs = pr_util.source_repository.scm_instance()
1422 1422 if backend.alias == 'git':
1423 1423 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1424 1424 else:
1425 1425 vcs.strip(pr_util.commit_ids['new-feature'])
1426 1426
1427 1427 url = route_path('pullrequest_update',
1428 1428 repo_name=target_repo_name,
1429 1429 pull_request_id=pr_id)
1430 1430 response = self.app.post(url,
1431 1431 params={'update_commits': 'true',
1432 1432 'csrf_token': csrf_token})
1433 1433
1434 1434 assert response.status_int == 200
1435 1435 assert json.loads(response.body) == json.loads('{"response": true, "redirect_url": null}')
1436 1436
1437 1437 # Make sure that after update, it won't raise 500 errors
1438 1438 response = self.app.get(route_path(
1439 1439 'pullrequest_show',
1440 1440 repo_name=target_repo_name,
1441 1441 pull_request_id=pr_id))
1442 1442
1443 1443 assert response.status_int == 200
1444 1444 response.assert_response().element_contains(
1445 1445 '#changeset_compare_view_content .alert strong',
1446 1446 'Missing commits')
1447 1447
1448 1448 def test_branch_is_a_link(self, pr_util):
1449 1449 pull_request = pr_util.create_pull_request()
1450 1450 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1451 1451 pull_request.target_ref = 'branch:target:abcdef1234567890'
1452 1452 Session().add(pull_request)
1453 1453 Session().commit()
1454 1454
1455 1455 response = self.app.get(route_path(
1456 1456 'pullrequest_show',
1457 1457 repo_name=pull_request.target_repo.scm_instance().name,
1458 1458 pull_request_id=pull_request.pull_request_id))
1459 1459 assert response.status_int == 200
1460 1460
1461 1461 source = response.assert_response().get_element('.pr-source-info')
1462 1462 source_parent = source.getparent()
1463 1463 assert len(source_parent) == 1
1464 1464
1465 1465 target = response.assert_response().get_element('.pr-target-info')
1466 1466 target_parent = target.getparent()
1467 1467 assert len(target_parent) == 1
1468 1468
1469 1469 expected_origin_link = route_path(
1470 1470 'repo_commits',
1471 1471 repo_name=pull_request.source_repo.scm_instance().name,
1472 1472 params=dict(branch='origin'))
1473 1473 expected_target_link = route_path(
1474 1474 'repo_commits',
1475 1475 repo_name=pull_request.target_repo.scm_instance().name,
1476 1476 params=dict(branch='target'))
1477 1477 assert source_parent.attrib['href'] == expected_origin_link
1478 1478 assert target_parent.attrib['href'] == expected_target_link
1479 1479
1480 1480 def test_bookmark_is_not_a_link(self, pr_util):
1481 1481 pull_request = pr_util.create_pull_request()
1482 1482 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1483 1483 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1484 1484 Session().add(pull_request)
1485 1485 Session().commit()
1486 1486
1487 1487 response = self.app.get(route_path(
1488 1488 'pullrequest_show',
1489 1489 repo_name=pull_request.target_repo.scm_instance().name,
1490 1490 pull_request_id=pull_request.pull_request_id))
1491 1491 assert response.status_int == 200
1492 1492
1493 1493 source = response.assert_response().get_element('.pr-source-info')
1494 1494 assert source.text.strip() == 'bookmark:origin'
1495 1495 assert source.getparent().attrib.get('href') is None
1496 1496
1497 1497 target = response.assert_response().get_element('.pr-target-info')
1498 1498 assert target.text.strip() == 'bookmark:target'
1499 1499 assert target.getparent().attrib.get('href') is None
1500 1500
1501 1501 def test_tag_is_not_a_link(self, pr_util):
1502 1502 pull_request = pr_util.create_pull_request()
1503 1503 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1504 1504 pull_request.target_ref = 'tag:target:abcdef1234567890'
1505 1505 Session().add(pull_request)
1506 1506 Session().commit()
1507 1507
1508 1508 response = self.app.get(route_path(
1509 1509 'pullrequest_show',
1510 1510 repo_name=pull_request.target_repo.scm_instance().name,
1511 1511 pull_request_id=pull_request.pull_request_id))
1512 1512 assert response.status_int == 200
1513 1513
1514 1514 source = response.assert_response().get_element('.pr-source-info')
1515 1515 assert source.text.strip() == 'tag:origin'
1516 1516 assert source.getparent().attrib.get('href') is None
1517 1517
1518 1518 target = response.assert_response().get_element('.pr-target-info')
1519 1519 assert target.text.strip() == 'tag:target'
1520 1520 assert target.getparent().attrib.get('href') is None
1521 1521
1522 1522 @pytest.mark.parametrize('mergeable', [True, False])
1523 1523 def test_shadow_repository_link(
1524 1524 self, mergeable, pr_util, http_host_only_stub):
1525 1525 """
1526 1526 Check that the pull request summary page displays a link to the shadow
1527 1527 repository if the pull request is mergeable. If it is not mergeable
1528 1528 the link should not be displayed.
1529 1529 """
1530 1530 pull_request = pr_util.create_pull_request(
1531 1531 mergeable=mergeable, enable_notifications=False)
1532 1532 target_repo = pull_request.target_repo.scm_instance()
1533 1533 pr_id = pull_request.pull_request_id
1534 1534 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1535 1535 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1536 1536
1537 1537 response = self.app.get(route_path(
1538 1538 'pullrequest_show',
1539 1539 repo_name=target_repo.name,
1540 1540 pull_request_id=pr_id))
1541 1541
1542 1542 if mergeable:
1543 1543 response.assert_response().element_value_contains(
1544 1544 'input.pr-mergeinfo', shadow_url)
1545 1545 response.assert_response().element_value_contains(
1546 1546 'input.pr-mergeinfo ', 'pr-merge')
1547 1547 else:
1548 1548 response.assert_response().no_element_exists('.pr-mergeinfo')
1549 1549
1550 1550
1551 1551 @pytest.mark.usefixtures('app')
1552 1552 @pytest.mark.backends("git", "hg")
1553 1553 class TestPullrequestsControllerDelete(object):
1554 1554 def test_pull_request_delete_button_permissions_admin(
1555 1555 self, autologin_user, user_admin, pr_util):
1556 1556 pull_request = pr_util.create_pull_request(
1557 1557 author=user_admin.username, enable_notifications=False)
1558 1558
1559 1559 response = self.app.get(route_path(
1560 1560 'pullrequest_show',
1561 1561 repo_name=pull_request.target_repo.scm_instance().name,
1562 1562 pull_request_id=pull_request.pull_request_id))
1563 1563
1564 1564 response.mustcontain('id="delete_pullrequest"')
1565 1565 response.mustcontain('Confirm to delete this pull request')
1566 1566
1567 1567 def test_pull_request_delete_button_permissions_owner(
1568 1568 self, autologin_regular_user, user_regular, pr_util):
1569 1569 pull_request = pr_util.create_pull_request(
1570 1570 author=user_regular.username, enable_notifications=False)
1571 1571
1572 1572 response = self.app.get(route_path(
1573 1573 'pullrequest_show',
1574 1574 repo_name=pull_request.target_repo.scm_instance().name,
1575 1575 pull_request_id=pull_request.pull_request_id))
1576 1576
1577 1577 response.mustcontain('id="delete_pullrequest"')
1578 1578 response.mustcontain('Confirm to delete this pull request')
1579 1579
1580 1580 def test_pull_request_delete_button_permissions_forbidden(
1581 1581 self, autologin_regular_user, user_regular, user_admin, pr_util):
1582 1582 pull_request = pr_util.create_pull_request(
1583 1583 author=user_admin.username, enable_notifications=False)
1584 1584
1585 1585 response = self.app.get(route_path(
1586 1586 'pullrequest_show',
1587 1587 repo_name=pull_request.target_repo.scm_instance().name,
1588 1588 pull_request_id=pull_request.pull_request_id))
1589 1589 response.mustcontain(no=['id="delete_pullrequest"'])
1590 1590 response.mustcontain(no=['Confirm to delete this pull request'])
1591 1591
1592 1592 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1593 1593 self, autologin_regular_user, user_regular, user_admin, pr_util,
1594 1594 user_util):
1595 1595
1596 1596 pull_request = pr_util.create_pull_request(
1597 1597 author=user_admin.username, enable_notifications=False)
1598 1598
1599 1599 user_util.grant_user_permission_to_repo(
1600 1600 pull_request.target_repo, user_regular,
1601 1601 'repository.write')
1602 1602
1603 1603 response = self.app.get(route_path(
1604 1604 'pullrequest_show',
1605 1605 repo_name=pull_request.target_repo.scm_instance().name,
1606 1606 pull_request_id=pull_request.pull_request_id))
1607 1607
1608 1608 response.mustcontain('id="open_edit_pullrequest"')
1609 1609 response.mustcontain('id="delete_pullrequest"')
1610 1610 response.mustcontain(no=['Confirm to delete this pull request'])
1611 1611
1612 1612 def test_delete_comment_returns_404_if_comment_does_not_exist(
1613 1613 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1614 1614
1615 1615 pull_request = pr_util.create_pull_request(
1616 1616 author=user_admin.username, enable_notifications=False)
1617 1617
1618 1618 self.app.post(
1619 1619 route_path(
1620 1620 'pullrequest_comment_delete',
1621 1621 repo_name=pull_request.target_repo.scm_instance().name,
1622 1622 pull_request_id=pull_request.pull_request_id,
1623 1623 comment_id=1024404),
1624 1624 extra_environ=xhr_header,
1625 1625 params={'csrf_token': csrf_token},
1626 1626 status=404
1627 1627 )
1628 1628
1629 1629 def test_delete_comment(
1630 1630 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1631 1631
1632 1632 pull_request = pr_util.create_pull_request(
1633 1633 author=user_admin.username, enable_notifications=False)
1634 1634 comment = pr_util.create_comment()
1635 1635 comment_id = comment.comment_id
1636 1636
1637 1637 response = self.app.post(
1638 1638 route_path(
1639 1639 'pullrequest_comment_delete',
1640 1640 repo_name=pull_request.target_repo.scm_instance().name,
1641 1641 pull_request_id=pull_request.pull_request_id,
1642 1642 comment_id=comment_id),
1643 1643 extra_environ=xhr_header,
1644 1644 params={'csrf_token': csrf_token},
1645 1645 status=200
1646 1646 )
1647 1647 assert response.body == 'true'
1648 1648
1649 1649 @pytest.mark.parametrize('url_type', [
1650 1650 'pullrequest_new',
1651 1651 'pullrequest_create',
1652 1652 'pullrequest_update',
1653 1653 'pullrequest_merge',
1654 1654 ])
1655 1655 def test_pull_request_is_forbidden_on_archived_repo(
1656 1656 self, autologin_user, backend, xhr_header, user_util, url_type):
1657 1657
1658 1658 # create a temporary repo
1659 1659 source = user_util.create_repo(repo_type=backend.alias)
1660 1660 repo_name = source.repo_name
1661 1661 repo = Repository.get_by_repo_name(repo_name)
1662 1662 repo.archived = True
1663 1663 Session().commit()
1664 1664
1665 1665 response = self.app.get(
1666 1666 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1667 1667
1668 1668 msg = 'Action not supported for archived repository.'
1669 1669 assert_session_flash(response, msg)
1670 1670
1671 1671
1672 1672 def assert_pull_request_status(pull_request, expected_status):
1673 1673 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1674 1674 assert status == expected_status
1675 1675
1676 1676
1677 1677 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1678 1678 @pytest.mark.usefixtures("autologin_user")
1679 1679 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1680 1680 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,232 +1,232 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.lib.utils2 import str2bool
25 25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.tests import (
29 29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
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
36 import urllib.request, urllib.parse, urllib.error
37 37
38 38 base_url = {
39 39 'edit_repo': '/{repo_name}/settings',
40 40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 41 'edit_repo_caches': '/{repo_name}/settings/caches',
42 42 'edit_repo_perms': '/{repo_name}/settings/permissions',
43 43 'edit_repo_vcs': '/{repo_name}/settings/vcs',
44 44 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
45 45 'edit_repo_fields': '/{repo_name}/settings/fields',
46 46 'edit_repo_remote': '/{repo_name}/settings/remote',
47 47 'edit_repo_statistics': '/{repo_name}/settings/statistics',
48 48 }[name].format(**kwargs)
49 49
50 50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 52 return base_url
53 53
54 54
55 55 def _get_permission_for_user(user, repo):
56 56 perm = UserRepoToPerm.query()\
57 57 .filter(UserRepoToPerm.repository ==
58 58 Repository.get_by_repo_name(repo))\
59 59 .filter(UserRepoToPerm.user == User.get_by_username(user))\
60 60 .all()
61 61 return perm
62 62
63 63
64 64 @pytest.mark.usefixtures('autologin_user', 'app')
65 65 class TestAdminRepoSettings(object):
66 66 @pytest.mark.parametrize('urlname', [
67 67 'edit_repo',
68 68 'edit_repo_caches',
69 69 'edit_repo_perms',
70 70 'edit_repo_advanced',
71 71 'edit_repo_vcs',
72 72 'edit_repo_issuetracker',
73 73 'edit_repo_fields',
74 74 'edit_repo_remote',
75 75 'edit_repo_statistics',
76 76 ])
77 77 def test_show_page(self, urlname, app, backend):
78 78 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
79 79
80 80 def test_edit_accessible_when_missing_requirements(
81 81 self, backend_hg, autologin_user):
82 82 scm_patcher = mock.patch.object(
83 83 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
84 84 with scm_patcher:
85 85 self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
86 86
87 87 @pytest.mark.parametrize('update_settings', [
88 88 {'repo_description': 'alter-desc'},
89 89 {'repo_owner': TEST_USER_REGULAR_LOGIN},
90 90 {'repo_private': 'true'},
91 91 {'repo_enable_locking': 'true'},
92 92 {'repo_enable_downloads': 'true'},
93 93 ])
94 94 def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
95 95 repo = user_util.create_repo(repo_type=backend.alias)
96 96 repo_name = repo.repo_name
97 97
98 98 params = fixture._get_repo_create_params(
99 99 csrf_token=csrf_token,
100 100 repo_name=repo_name,
101 101 repo_type=backend.alias,
102 102 repo_owner=TEST_USER_ADMIN_LOGIN,
103 103 repo_description='DESC',
104 104
105 105 repo_private='false',
106 106 repo_enable_locking='false',
107 107 repo_enable_downloads='false')
108 108 params.update(update_settings)
109 109 self.app.post(
110 110 route_path('edit_repo', repo_name=repo_name),
111 111 params=params, status=302)
112 112
113 113 repo = Repository.get_by_repo_name(repo_name)
114 114 assert repo.user.username == \
115 115 update_settings.get('repo_owner', repo.user.username)
116 116
117 117 assert repo.description == \
118 118 update_settings.get('repo_description', repo.description)
119 119
120 120 assert repo.private == \
121 121 str2bool(update_settings.get(
122 122 'repo_private', repo.private))
123 123
124 124 assert repo.enable_locking == \
125 125 str2bool(update_settings.get(
126 126 'repo_enable_locking', repo.enable_locking))
127 127
128 128 assert repo.enable_downloads == \
129 129 str2bool(update_settings.get(
130 130 'repo_enable_downloads', repo.enable_downloads))
131 131
132 132 def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
133 133 repo = user_util.create_repo(repo_type=backend.alias)
134 134 repo_name = repo.repo_name
135 135
136 136 repo_group = user_util.create_repo_group()
137 137 repo_group_name = repo_group.group_name
138 138 new_name = repo_group_name + '_' + repo_name
139 139
140 140 params = fixture._get_repo_create_params(
141 141 csrf_token=csrf_token,
142 142 repo_name=new_name,
143 143 repo_type=backend.alias,
144 144 repo_owner=TEST_USER_ADMIN_LOGIN,
145 145 repo_description='DESC',
146 146 repo_private='false',
147 147 repo_enable_locking='false',
148 148 repo_enable_downloads='false')
149 149 self.app.post(
150 150 route_path('edit_repo', repo_name=repo_name),
151 151 params=params, status=302)
152 152 repo = Repository.get_by_repo_name(new_name)
153 153 assert repo.repo_name == new_name
154 154
155 155 def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
156 156 repo = user_util.create_repo(repo_type=backend.alias)
157 157 repo_name = repo.repo_name
158 158
159 159 repo_group = user_util.create_repo_group()
160 160 repo_group_name = repo_group.group_name
161 161 repo_group_id = repo_group.group_id
162 162
163 163 new_name = repo_group_name + '/' + repo_name
164 164 params = fixture._get_repo_create_params(
165 165 csrf_token=csrf_token,
166 166 repo_name=repo_name,
167 167 repo_type=backend.alias,
168 168 repo_owner=TEST_USER_ADMIN_LOGIN,
169 169 repo_description='DESC',
170 170 repo_group=repo_group_id,
171 171 repo_private='false',
172 172 repo_enable_locking='false',
173 173 repo_enable_downloads='false')
174 174 self.app.post(
175 175 route_path('edit_repo', repo_name=repo_name),
176 176 params=params, status=302)
177 177 repo = Repository.get_by_repo_name(new_name)
178 178 assert repo.repo_name == new_name
179 179
180 180 def test_set_private_flag_sets_default_user_permissions_to_none(
181 181 self, autologin_user, backend, csrf_token):
182 182
183 183 # initially repository perm should be read
184 184 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
185 185 assert len(perm) == 1
186 186 assert perm[0].permission.permission_name == 'repository.read'
187 187 assert not backend.repo.private
188 188
189 189 response = self.app.post(
190 190 route_path('edit_repo', repo_name=backend.repo_name),
191 191 params=fixture._get_repo_create_params(
192 192 repo_private='true',
193 193 repo_name=backend.repo_name,
194 194 repo_type=backend.alias,
195 195 repo_owner=TEST_USER_ADMIN_LOGIN,
196 196 csrf_token=csrf_token), status=302)
197 197
198 198 assert_session_flash(
199 199 response,
200 200 msg='Repository `%s` updated successfully' % (backend.repo_name))
201 201
202 202 repo = Repository.get_by_repo_name(backend.repo_name)
203 203 assert repo.private is True
204 204
205 205 # now the repo default permission should be None
206 206 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
207 207 assert len(perm) == 1
208 208 assert perm[0].permission.permission_name == 'repository.none'
209 209
210 210 response = self.app.post(
211 211 route_path('edit_repo', repo_name=backend.repo_name),
212 212 params=fixture._get_repo_create_params(
213 213 repo_private='false',
214 214 repo_name=backend.repo_name,
215 215 repo_type=backend.alias,
216 216 repo_owner=TEST_USER_ADMIN_LOGIN,
217 217 csrf_token=csrf_token), status=302)
218 218
219 219 assert_session_flash(
220 220 response,
221 221 msg='Repository `%s` updated successfully' % (backend.repo_name))
222 222 assert backend.repo.private is False
223 223
224 224 # we turn off private now the repo default permission should stay None
225 225 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
226 226 assert len(perm) == 1
227 227 assert perm[0].permission.permission_name == 'repository.none'
228 228
229 229 # update this permission back
230 230 perm[0].permission = Permission.get_by_key('repository.read')
231 231 Session().add(perm[0])
232 232 Session().commit()
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