##// END OF EJS Templates
tests: refactor code to use a single test url generator
super-admin -
r5173:95a4b30f default
parent child Browse files
Show More
@@ -0,0 +1,290 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19
20 def get_url_defs():
21 from rhodecode.apps._base import ADMIN_PREFIX
22
23 return {
24 "home": "/",
25 "main_page_repos_data": "/_home_repos",
26 "main_page_repo_groups_data": "/_home_repo_groups",
27 "repo_group_home": "/{repo_group_name}",
28 "user_autocomplete_data": "/_users",
29 "user_group_autocomplete_data": "/_user_groups",
30 "repo_list_data": "/_repos",
31 "goto_switcher_data": "/_goto_data",
32 "admin_home": ADMIN_PREFIX + "",
33 "admin_audit_logs": ADMIN_PREFIX + "/audit_logs",
34 "admin_defaults_repositories": ADMIN_PREFIX + "/defaults/repositories",
35 "admin_defaults_repositories_update": ADMIN_PREFIX + "/defaults/repositories/update",
36 "search": ADMIN_PREFIX + "/search",
37 "search_repo": "/{repo_name}/search",
38 "my_account_auth_tokens": ADMIN_PREFIX + "/my_account/auth_tokens",
39 "my_account_auth_tokens_add": ADMIN_PREFIX + "/my_account/auth_tokens/new",
40 "my_account_auth_tokens_delete": ADMIN_PREFIX + "/my_account/auth_tokens/delete",
41 "repos": ADMIN_PREFIX + "/repos",
42 "repos_data": ADMIN_PREFIX + "/repos_data",
43 "repo_groups": ADMIN_PREFIX + "/repo_groups",
44 "repo_groups_data": ADMIN_PREFIX + "/repo_groups_data",
45 "user_groups": ADMIN_PREFIX + "/user_groups",
46 "user_groups_data": ADMIN_PREFIX + "/user_groups_data",
47 "user_profile": "/_profiles/{username}",
48 "profile_user_group": "/_profile_user_group/{user_group_name}",
49 "repo_summary": "/{repo_name}",
50 "repo_creating_check": "/{repo_name}/repo_creating_check",
51 "edit_repo": "/{repo_name}/settings",
52 "edit_repo_vcs": "/{repo_name}/settings/vcs",
53 "edit_repo_vcs_update": "/{repo_name}/settings/vcs/update",
54 "edit_repo_vcs_svn_pattern_delete": "/{repo_name}/settings/vcs/svn_pattern/delete",
55 "repo_archivefile": "/{repo_name}/archive/{fname}",
56 "repo_files_diff": "/{repo_name}/diff/{f_path}",
57 "repo_files_diff_2way_redirect": "/{repo_name}/diff-2way/{f_path}",
58 "repo_files": "/{repo_name}/files/{commit_id}/{f_path}",
59 "repo_files:default_path": "/{repo_name}/files/{commit_id}/",
60 "repo_files:default_commit": "/{repo_name}/files",
61 "repo_files:rendered": "/{repo_name}/render/{commit_id}/{f_path}",
62 "repo_files:annotated": "/{repo_name}/annotate/{commit_id}/{f_path}",
63 "repo_files:annotated_previous": "/{repo_name}/annotate-previous/{commit_id}/{f_path}",
64 "repo_files_nodelist": "/{repo_name}/nodelist/{commit_id}/{f_path}",
65 "repo_file_raw": "/{repo_name}/raw/{commit_id}/{f_path}",
66 "repo_file_download": "/{repo_name}/download/{commit_id}/{f_path}",
67 "repo_file_history": "/{repo_name}/history/{commit_id}/{f_path}",
68 "repo_file_authors": "/{repo_name}/authors/{commit_id}/{f_path}",
69 "repo_files_remove_file": "/{repo_name}/remove_file/{commit_id}/{f_path}",
70 "repo_files_delete_file": "/{repo_name}/delete_file/{commit_id}/{f_path}",
71 "repo_files_edit_file": "/{repo_name}/edit_file/{commit_id}/{f_path}",
72 "repo_files_update_file": "/{repo_name}/update_file/{commit_id}/{f_path}",
73 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
74 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
75 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
76 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
77 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
78 "journal": ADMIN_PREFIX + "/journal",
79 "journal_rss": ADMIN_PREFIX + "/journal/rss",
80 "journal_atom": ADMIN_PREFIX + "/journal/atom",
81 "journal_public": ADMIN_PREFIX + "/public_journal",
82 "journal_public_atom": ADMIN_PREFIX + "/public_journal/atom",
83 "journal_public_atom_old": ADMIN_PREFIX + "/public_journal_atom",
84 "journal_public_rss": ADMIN_PREFIX + "/public_journal/rss",
85 "journal_public_rss_old": ADMIN_PREFIX + "/public_journal_rss",
86 "toggle_following": ADMIN_PREFIX + "/toggle_following",
87 "upload_file": "/_file_store/upload",
88 "download_file": "/_file_store/download/{fid}",
89 "download_file_by_token": "/_file_store/token-download/{_auth_token}/{fid}",
90 "gists_show": ADMIN_PREFIX + "/gists",
91 "gists_new": ADMIN_PREFIX + "/gists/new",
92 "gists_create": ADMIN_PREFIX + "/gists/create",
93 "gist_show": ADMIN_PREFIX + "/gists/{gist_id}",
94 "gist_delete": ADMIN_PREFIX + "/gists/{gist_id}/delete",
95 "gist_edit": ADMIN_PREFIX + "/gists/{gist_id}/edit",
96 "gist_edit_check_revision": ADMIN_PREFIX + "/gists/{gist_id}/edit/check_revision",
97 "gist_update": ADMIN_PREFIX + "/gists/{gist_id}/update",
98 "gist_show_rev": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}",
99 "gist_show_formatted": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}/{format}",
100 "gist_show_formatted_path": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
101 "login": ADMIN_PREFIX + "/login",
102 "logout": ADMIN_PREFIX + "/logout",
103 "register": ADMIN_PREFIX + "/register",
104 "reset_password": ADMIN_PREFIX + "/password_reset",
105 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
106 "admin_permissions_application": ADMIN_PREFIX + "/permissions/application",
107 "admin_permissions_application_update": ADMIN_PREFIX + "/permissions/application/update",
108 "repo_commit_raw": "/{repo_name}/changeset-diff/{commit_id}",
109 "user_group_members_data": ADMIN_PREFIX + "/user_groups/{user_group_id}/members",
110 "user_groups_new": ADMIN_PREFIX + "/user_groups/new",
111 "user_groups_create": ADMIN_PREFIX + "/user_groups/create",
112 "edit_user_group": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit",
113 "edit_user_group_advanced_sync": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit/advanced/sync",
114 "edit_user_group_global_perms_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit/global_permissions/update",
115 "user_groups_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/update",
116 "user_groups_delete": ADMIN_PREFIX + "/user_groups/{user_group_id}/delete",
117 "edit_user_group_perms": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit/permissions",
118 "edit_user_group_perms_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit/permissions/update",
119 "edit_repo_group": "/{repo_group_name}/_edit",
120 "edit_repo_group_perms": "/{repo_group_name:}/_settings/permissions",
121 "edit_repo_group_perms_update": "/{repo_group_name}/_settings/permissions/update",
122 "edit_repo_group_advanced": "/{repo_group_name}/_settings/advanced",
123 "edit_repo_group_advanced_delete": "/{repo_group_name}/_settings/advanced/delete",
124 "edit_user_ssh_keys": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys",
125 "edit_user_ssh_keys_generate_keypair": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/generate",
126 "edit_user_ssh_keys_add": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/new",
127 "edit_user_ssh_keys_delete": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/delete",
128 "users": ADMIN_PREFIX + "/users",
129 "users_data": ADMIN_PREFIX + "/users_data",
130 "users_create": ADMIN_PREFIX + "/users/create",
131 "users_new": ADMIN_PREFIX + "/users/new",
132 "user_edit": ADMIN_PREFIX + "/users/{user_id}/edit",
133 "user_edit_advanced": ADMIN_PREFIX + "/users/{user_id}/edit/advanced",
134 "user_edit_global_perms": ADMIN_PREFIX + "/users/{user_id}/edit/global_permissions",
135 "user_edit_global_perms_update": ADMIN_PREFIX + "/users/{user_id}/edit/global_permissions/update",
136 "user_update": ADMIN_PREFIX + "/users/{user_id}/update",
137 "user_delete": ADMIN_PREFIX + "/users/{user_id}/delete",
138 "user_create_personal_repo_group": ADMIN_PREFIX + "/users/{user_id}/create_repo_group",
139 "edit_user_auth_tokens": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens",
140 "edit_user_auth_tokens_add": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens/new",
141 "edit_user_auth_tokens_delete": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens/delete",
142 "edit_user_emails": ADMIN_PREFIX + "/users/{user_id}/edit/emails",
143 "edit_user_emails_add": ADMIN_PREFIX + "/users/{user_id}/edit/emails/new",
144 "edit_user_emails_delete": ADMIN_PREFIX + "/users/{user_id}/edit/emails/delete",
145 "edit_user_ips": ADMIN_PREFIX + "/users/{user_id}/edit/ips",
146 "edit_user_ips_add": ADMIN_PREFIX + "/users/{user_id}/edit/ips/new",
147 "edit_user_ips_delete": ADMIN_PREFIX + "/users/{user_id}/edit/ips/delete",
148 "edit_user_perms_summary": ADMIN_PREFIX + "/users/{user_id}/edit/permissions_summary",
149 "edit_user_perms_summary_json": ADMIN_PREFIX + "/users/{user_id}/edit/permissions_summary/json",
150 "edit_user_audit_logs": ADMIN_PREFIX + "/users/{user_id}/edit/audit",
151 "edit_user_audit_logs_download": ADMIN_PREFIX + "/users/{user_id}/edit/audit/download",
152 "admin_settings": ADMIN_PREFIX + "/settings",
153 "admin_settings_update": ADMIN_PREFIX + "/settings/update",
154 "admin_settings_global": ADMIN_PREFIX + "/settings/global",
155 "admin_settings_global_update": ADMIN_PREFIX + "/settings/global/update",
156 "admin_settings_vcs": ADMIN_PREFIX + "/settings/vcs",
157 "admin_settings_vcs_update": ADMIN_PREFIX + "/settings/vcs/update",
158 "admin_settings_vcs_svn_pattern_delete": ADMIN_PREFIX + "/settings/vcs/svn_pattern_delete",
159 "admin_settings_mapping": ADMIN_PREFIX + "/settings/mapping",
160 "admin_settings_mapping_update": ADMIN_PREFIX + "/settings/mapping/update",
161 "admin_settings_visual": ADMIN_PREFIX + "/settings/visual",
162 "admin_settings_visual_update": ADMIN_PREFIX + "/settings/visual/update",
163 "admin_settings_issuetracker": ADMIN_PREFIX + "/settings/issue-tracker",
164 "admin_settings_issuetracker_update": ADMIN_PREFIX + "/settings/issue-tracker/update",
165 "admin_settings_issuetracker_test": ADMIN_PREFIX + "/settings/issue-tracker/test",
166 "admin_settings_issuetracker_delete": ADMIN_PREFIX + "/settings/issue-tracker/delete",
167 "admin_settings_email": ADMIN_PREFIX + "/settings/email",
168 "admin_settings_email_update": ADMIN_PREFIX + "/settings/email/update",
169 "admin_settings_hooks": ADMIN_PREFIX + "/settings/hooks",
170 "admin_settings_hooks_update": ADMIN_PREFIX + "/settings/hooks/update",
171 "admin_settings_hooks_delete": ADMIN_PREFIX + "/settings/hooks/delete",
172 "admin_settings_search": ADMIN_PREFIX + "/settings/search",
173 "admin_settings_labs": ADMIN_PREFIX + "/settings/labs",
174 "admin_settings_labs_update": ADMIN_PREFIX + "/settings/labs/update",
175 "admin_settings_sessions": ADMIN_PREFIX + "/settings/sessions",
176 "admin_settings_sessions_cleanup": ADMIN_PREFIX + "/settings/sessions/cleanup",
177 "admin_settings_system": ADMIN_PREFIX + "/settings/system",
178 "admin_settings_system_update": ADMIN_PREFIX + "/settings/system/updates",
179 "admin_settings_open_source": ADMIN_PREFIX + "/settings/open_source",
180 "repo_group_new": ADMIN_PREFIX + "/repo_group/new",
181 "repo_group_create": ADMIN_PREFIX + "/repo_group/create",
182 "repo_new": ADMIN_PREFIX + "/repos/new",
183 "repo_create": ADMIN_PREFIX + "/repos/create",
184 "admin_permissions_global": ADMIN_PREFIX + "/permissions/global",
185 "admin_permissions_global_update": ADMIN_PREFIX + "/permissions/global/update",
186 "admin_permissions_object": ADMIN_PREFIX + "/permissions/object",
187 "admin_permissions_object_update": ADMIN_PREFIX + "/permissions/object/update",
188 "admin_permissions_ips": ADMIN_PREFIX + "/permissions/ips",
189 "admin_permissions_overview": ADMIN_PREFIX + "/permissions/overview",
190 "admin_permissions_ssh_keys": ADMIN_PREFIX + "/permissions/ssh_keys",
191 "admin_permissions_ssh_keys_data": ADMIN_PREFIX + "/permissions/ssh_keys/data",
192 "admin_permissions_ssh_keys_update": ADMIN_PREFIX + "/permissions/ssh_keys/update",
193 "pullrequest_show": "/{repo_name}/pull-request/{pull_request_id}",
194 "pull_requests_global": ADMIN_PREFIX + "/pull-request/{pull_request_id}",
195 "pull_requests_global_0": ADMIN_PREFIX + "/pull_requests/{pull_request_id}",
196 "pull_requests_global_1": ADMIN_PREFIX + "/pull-requests/{pull_request_id}",
197 "notifications_show_all": ADMIN_PREFIX + "/notifications",
198 "notifications_mark_all_read": ADMIN_PREFIX + "/notifications_mark_all_read",
199 "notifications_show": ADMIN_PREFIX + "/notifications/{notification_id}",
200 "notifications_update": ADMIN_PREFIX + "/notifications/{notification_id}/update",
201 "notifications_delete": ADMIN_PREFIX + "/notifications/{notification_id}/delete",
202 "my_account": ADMIN_PREFIX + "/my_account/profile",
203 "my_account_edit": ADMIN_PREFIX + "/my_account/edit",
204 "my_account_update": ADMIN_PREFIX + "/my_account/update",
205 "my_account_pullrequests": ADMIN_PREFIX + "/my_account/pull_requests",
206 "my_account_pullrequests_data": ADMIN_PREFIX + "/my_account/pull_requests/data",
207 "my_account_emails": ADMIN_PREFIX + "/my_account/emails",
208 "my_account_emails_add": ADMIN_PREFIX + "/my_account/emails/new",
209 "my_account_emails_delete": ADMIN_PREFIX + "/my_account/emails/delete",
210 "my_account_password": ADMIN_PREFIX + "/my_account/password",
211 "my_account_password_update": ADMIN_PREFIX + "/my_account/password/update",
212 "my_account_repos": ADMIN_PREFIX + "/my_account/repos",
213 "my_account_watched": ADMIN_PREFIX + "/my_account/watched",
214 "my_account_perms": ADMIN_PREFIX + "/my_account/perms",
215 "my_account_notifications": ADMIN_PREFIX + "/my_account/notifications",
216 "my_account_ssh_keys": ADMIN_PREFIX + "/my_account/ssh_keys",
217 "my_account_ssh_keys_generate": ADMIN_PREFIX + "/my_account/ssh_keys/generate",
218 "my_account_ssh_keys_add": ADMIN_PREFIX + "/my_account/ssh_keys/new",
219 "my_account_ssh_keys_delete": ADMIN_PREFIX + "/my_account/ssh_keys/delete",
220 "pullrequest_show_all": "/{repo_name}/pull-request",
221 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
222 "bookmarks_home": "/{repo_name}/bookmarks",
223 "branches_home": "/{repo_name}/branches",
224 "tags_home": "/{repo_name}/tags",
225 "repo_changelog": "/{repo_name}/changelog",
226 "repo_commits": "/{repo_name}/commits",
227 "repo_commits_file": "/{repo_name}/commits/{commit_id}/{f_path}",
228 "repo_commits_elements": "/{repo_name}/commits_elements",
229 "repo_commit": "/{repo_name}/changeset/{commit_id}",
230 "repo_commit_comment_create": "/{repo_name}/changeset/{commit_id}/comment/create",
231 "repo_commit_comment_preview": "/{repo_name}/changeset/{commit_id}/comment/preview",
232 "repo_commit_comment_delete": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete",
233 "repo_commit_comment_edit": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit",
234 "repo_commit_children": "/{repo_name}/changeset_children/{commit_id}",
235 "repo_commit_parents": "/{repo_name}/changeset_parents/{commit_id}",
236 "repo_commit_patch": "/{repo_name}/changeset-patch/{commit_id}",
237 "repo_commit_download": "/{repo_name}/changeset-download/{commit_id}",
238 "repo_commit_data": "/{repo_name}/changeset-data/{commit_id}",
239 "repo_compare": "/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}",
240 "repo_compare_select": "/{repo_name}/compare",
241 "rss_feed_home": "/{repo_name}/feed-rss",
242 "atom_feed_home": "/{repo_name}/feed-atom",
243 "rss_feed_home_old": "/{repo_name}/feed/rss",
244 "atom_feed_home_old": "/{repo_name}/feed/atom",
245 "repo_fork_new": "/{repo_name}/fork",
246 "repo_fork_create": "/{repo_name}/fork/create",
247 "repo_forks_show_all": "/{repo_name}/forks",
248 "repo_forks_data": "/{repo_name}/forks/data",
249 "edit_repo_issuetracker": "/{repo_name}/settings/issue_trackers",
250 "edit_repo_issuetracker_test": "/{repo_name}/settings/issue_trackers/test",
251 "edit_repo_issuetracker_delete": "/{repo_name}/settings/issue_trackers/delete",
252 "edit_repo_issuetracker_update": "/{repo_name}/settings/issue_trackers/update",
253 "edit_repo_maintenance": "/{repo_name}/settings/maintenance",
254 "edit_repo_maintenance_execute": "/{repo_name}/settings/maintenance/execute",
255 "repo_changelog_file": "/{repo_name}/changelog/{commit_id}/{f_path}",
256 "pullrequest_repo_refs": "/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}",
257 "pullrequest_repo_targets": "/{repo_name}/pull-request/repo-destinations",
258 "pullrequest_new": "/{repo_name}/pull-request/new",
259 "pullrequest_create": "/{repo_name}/pull-request/create",
260 "pullrequest_update": "/{repo_name}/pull-request/{pull_request_id}/update",
261 "pullrequest_merge": "/{repo_name}/pull-request/{pull_request_id}/merge",
262 "pullrequest_delete": "/{repo_name}/pull-request/{pull_request_id}/delete",
263 "pullrequest_comment_create": "/{repo_name}/pull-request/{pull_request_id}/comment",
264 "pullrequest_comment_delete": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete",
265 "pullrequest_comment_edit": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit",
266 "edit_repo_caches": "/{repo_name}/settings/caches",
267 "edit_repo_perms": "/{repo_name}/settings/permissions",
268 "edit_repo_fields": "/{repo_name}/settings/fields",
269 "edit_repo_remote": "/{repo_name}/settings/remote",
270 "edit_repo_statistics": "/{repo_name}/settings/statistics",
271 "edit_repo_advanced": "/{repo_name}/settings/advanced",
272 "edit_repo_advanced_delete": "/{repo_name}/settings/advanced/delete",
273 "edit_repo_advanced_archive": "/{repo_name}/settings/advanced/archive",
274 "edit_repo_advanced_fork": "/{repo_name}/settings/advanced/fork",
275 "edit_repo_advanced_locking": "/{repo_name}/settings/advanced/locking",
276 "edit_repo_advanced_journal": "/{repo_name}/settings/advanced/journal",
277 "repo_stats": "/{repo_name}/repo_stats/{commit_id}",
278 "repo_refs_data": "/{repo_name}/refs-data",
279 "repo_refs_changelog_data": "/{repo_name}/refs-data-changelog"
280 }
281
282
283 def route_path(name, params=None, **kwargs):
284 import urllib.parse
285
286 base_url = get_url_defs()[name].format(**kwargs)
287
288 if params:
289 base_url = f"{base_url}?{urllib.parse.urlencode(params)}"
290 return base_url
@@ -1,574 +1,573 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import sys
22 22 import fnmatch
23 23
24 24 import decorator
25 25 import typing
26 26 import venusian
27 27 from collections import OrderedDict
28 28
29 29 from pyramid.exceptions import ConfigurationError
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32 from pyramid.httpexceptions import HTTPNotFound
33 33
34 34 from rhodecode.api.exc import (
35 35 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
36 36 from rhodecode.apps._base import TemplateArgs
37 37 from rhodecode.lib.auth import AuthUser
38 38 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
39 39 from rhodecode.lib.exc_tracking import store_exception
40 40 from rhodecode.lib import ext_json
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.lib.plugins.utils import get_plugin_settings
43 43 from rhodecode.model.db import User, UserApiKeys
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 48 DEFAULT_URL = '/_admin/apiv2'
49 49
50 50
51 51 def find_methods(jsonrpc_methods, pattern):
52 52 matches = OrderedDict()
53 53 if not isinstance(pattern, (list, tuple)):
54 54 pattern = [pattern]
55 55
56 56 for single_pattern in pattern:
57 57 for method_name, method in jsonrpc_methods.items():
58 58 if fnmatch.fnmatch(method_name, single_pattern):
59 59 matches[method_name] = method
60 60 return matches
61 61
62 62
63 63 class ExtJsonRenderer(object):
64 64 """
65 65 Custom renderer that makes use of our ext_json lib
66 66
67 67 """
68 68
69 69 def __init__(self):
70 70 self.serializer = ext_json.formatted_json
71 71
72 72 def __call__(self, info):
73 73 """ Returns a plain JSON-encoded string with content-type
74 74 ``application/json``. The content-type may be overridden by
75 75 setting ``request.response.content_type``."""
76 76
77 77 def _render(value, system):
78 78 request = system.get('request')
79 79 if request is not None:
80 80 response = request.response
81 81 ct = response.content_type
82 82 if ct == response.default_content_type:
83 83 response.content_type = 'application/json'
84 84
85 85 return self.serializer(value)
86 86
87 87 return _render
88 88
89 89
90 90 def jsonrpc_response(request, result):
91 91 rpc_id = getattr(request, 'rpc_id', None)
92 92
93 93 ret_value = ''
94 94 if rpc_id:
95 95 ret_value = {'id': rpc_id, 'result': result, 'error': None}
96 96
97 97 # fetch deprecation warnings, and store it inside results
98 98 deprecation = getattr(request, 'rpc_deprecation', None)
99 99 if deprecation:
100 100 ret_value['DEPRECATION_WARNING'] = deprecation
101 101
102 102 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
103 103 content_type = 'application/json'
104 104 content_type_header = 'Content-Type'
105 105 headers = {
106 106 content_type_header: content_type
107 107 }
108 108 return Response(
109 109 body=raw_body,
110 110 content_type=content_type,
111 111 headerlist=[(k, v) for k, v in headers.items()]
112 112 )
113 113
114 114
115 115 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
116 116 """
117 117 Generate a Response object with a JSON-RPC error body
118 118 """
119 119 headers = headers or {}
120 120 content_type = 'application/json'
121 121 content_type_header = 'Content-Type'
122 122 if content_type_header not in headers:
123 123 headers[content_type_header] = content_type
124 124
125 125 err_dict = {'id': retid, 'result': None, 'error': message}
126 126 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
127 127
128 128 return Response(
129 129 body=raw_body,
130 130 status=code,
131 131 content_type=content_type,
132 132 headerlist=[(k, v) for k, v in headers.items()]
133 133 )
134 134
135 135
136 136 def exception_view(exc, request):
137 137 rpc_id = getattr(request, 'rpc_id', None)
138 138
139 139 if isinstance(exc, JSONRPCError):
140 140 fault_message = safe_str(exc)
141 141 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
142 142 elif isinstance(exc, JSONRPCValidationError):
143 143 colander_exc = exc.colander_exception
144 144 # TODO(marcink): think maybe of nicer way to serialize errors ?
145 145 fault_message = colander_exc.asdict()
146 146 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
147 147 elif isinstance(exc, JSONRPCForbidden):
148 148 fault_message = 'Access was denied to this resource.'
149 149 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
150 150 elif isinstance(exc, HTTPNotFound):
151 151 method = request.rpc_method
152 152 log.debug('json-rpc method `%s` not found in list of '
153 153 'api calls: %s, rpc_id:%s',
154 154 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
155 155
156 156 similar = 'none'
157 157 try:
158 158 similar_paterns = [f'*{x}*' for x in method.split('_')]
159 159 similar_found = find_methods(
160 160 request.registry.jsonrpc_methods, similar_paterns)
161 161 similar = ', '.join(similar_found.keys()) or similar
162 162 except Exception:
163 163 # make the whole above block safe
164 164 pass
165 165
166 fault_message = "No such method: {}. Similar methods: {}".format(
167 method, similar)
166 fault_message = f"No such method: {method}. Similar methods: {similar}"
168 167 else:
169 168 fault_message = 'undefined error'
170 169 exc_info = exc.exc_info()
171 170 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
172 171
173 172 statsd = request.registry.statsd
174 173 if statsd:
175 174 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
176 175 statsd.incr('rhodecode_exception_total',
177 176 tags=["exc_source:api", f"type:{exc_type}"])
178 177
179 178 return jsonrpc_error(request, fault_message, rpc_id)
180 179
181 180
182 181 def request_view(request):
183 182 """
184 183 Main request handling method. It handles all logic to call a specific
185 184 exposed method
186 185 """
187 186 # cython compatible inspect
188 187 from rhodecode.config.patches import inspect_getargspec
189 188 inspect = inspect_getargspec()
190 189
191 190 # check if we can find this session using api_key, get_by_auth_token
192 191 # search not expired tokens only
193 192 try:
194 193 api_user = User.get_by_auth_token(request.rpc_api_key)
195 194
196 195 if api_user is None:
197 196 return jsonrpc_error(
198 197 request, retid=request.rpc_id, message='Invalid API KEY')
199 198
200 199 if not api_user.active:
201 200 return jsonrpc_error(
202 201 request, retid=request.rpc_id,
203 202 message='Request from this user not allowed')
204 203
205 204 # check if we are allowed to use this IP
206 205 auth_u = AuthUser(
207 206 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
208 207 if not auth_u.ip_allowed:
209 208 return jsonrpc_error(
210 209 request, retid=request.rpc_id,
211 210 message='Request from IP:{} not allowed'.format(
212 211 request.rpc_ip_addr))
213 212 else:
214 213 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
215 214
216 215 # register our auth-user
217 216 request.rpc_user = auth_u
218 217 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
219 218
220 219 # now check if token is valid for API
221 220 auth_token = request.rpc_api_key
222 221 token_match = api_user.authenticate_by_token(
223 222 auth_token, roles=[UserApiKeys.ROLE_API])
224 223 invalid_token = not token_match
225 224
226 225 log.debug('Checking if API KEY is valid with proper role')
227 226 if invalid_token:
228 227 return jsonrpc_error(
229 228 request, retid=request.rpc_id,
230 229 message='API KEY invalid or, has bad role for an API call')
231 230
232 231 except Exception:
233 232 log.exception('Error on API AUTH')
234 233 return jsonrpc_error(
235 234 request, retid=request.rpc_id, message='Invalid API KEY')
236 235
237 236 method = request.rpc_method
238 237 func = request.registry.jsonrpc_methods[method]
239 238
240 239 # now that we have a method, add request._req_params to
241 240 # self.kargs and dispatch control to WGIController
242 241
243 242 argspec = inspect.getargspec(func)
244 243 arglist = argspec[0]
245 244 defs = argspec[3] or []
246 245 defaults = [type(a) for a in defs]
247 246 default_empty = type(NotImplemented)
248 247
249 248 # kw arguments required by this method
250 249 func_kwargs = dict(itertools.zip_longest(
251 250 reversed(arglist), reversed(defaults), fillvalue=default_empty))
252 251
253 252 # This attribute will need to be first param of a method that uses
254 253 # api_key, which is translated to instance of user at that name
255 254 user_var = 'apiuser'
256 255 request_var = 'request'
257 256
258 257 for arg in [user_var, request_var]:
259 258 if arg not in arglist:
260 259 return jsonrpc_error(
261 260 request,
262 261 retid=request.rpc_id,
263 262 message='This method [%s] does not support '
264 263 'required parameter `%s`' % (func.__name__, arg))
265 264
266 265 # get our arglist and check if we provided them as args
267 266 for arg, default in func_kwargs.items():
268 267 if arg in [user_var, request_var]:
269 268 # user_var and request_var are pre-hardcoded parameters and we
270 269 # don't need to do any translation
271 270 continue
272 271
273 272 # skip the required param check if it's default value is
274 273 # NotImplementedType (default_empty)
275 274 if default == default_empty and arg not in request.rpc_params:
276 275 return jsonrpc_error(
277 276 request,
278 277 retid=request.rpc_id,
279 278 message=('Missing non optional `%s` arg in JSON DATA' % arg)
280 279 )
281 280
282 281 # sanitize extra passed arguments
283 282 for k in list(request.rpc_params.keys()):
284 283 if k not in func_kwargs:
285 284 del request.rpc_params[k]
286 285
287 286 call_params = request.rpc_params
288 287 call_params.update({
289 288 'request': request,
290 289 'apiuser': auth_u
291 290 })
292 291
293 292 # register some common functions for usage
294 293 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
295 294
296 295 statsd = request.registry.statsd
297 296
298 297 try:
299 298 ret_value = func(**call_params)
300 299 resp = jsonrpc_response(request, ret_value)
301 300 if statsd:
302 301 statsd.incr('rhodecode_api_call_success_total')
303 302 return resp
304 303 except JSONRPCBaseError:
305 304 raise
306 305 except Exception:
307 306 log.exception('Unhandled exception occurred on api call: %s', func)
308 307 exc_info = sys.exc_info()
309 308 exc_id, exc_type_name = store_exception(
310 309 id(exc_info), exc_info, prefix='rhodecode-api')
311 310 error_headers = {
312 311 'RhodeCode-Exception-Id': str(exc_id),
313 312 'RhodeCode-Exception-Type': str(exc_type_name)
314 313 }
315 314 err_resp = jsonrpc_error(
316 315 request, retid=request.rpc_id, message='Internal server error',
317 316 headers=error_headers)
318 317 if statsd:
319 318 statsd.incr('rhodecode_api_call_fail_total')
320 319 return err_resp
321 320
322 321
323 322 def setup_request(request):
324 323 """
325 324 Parse a JSON-RPC request body. It's used inside the predicates method
326 325 to validate and bootstrap requests for usage in rpc calls.
327 326
328 327 We need to raise JSONRPCError here if we want to return some errors back to
329 328 user.
330 329 """
331 330
332 331 log.debug('Executing setup request: %r', request)
333 332 request.rpc_ip_addr = get_ip_addr(request.environ)
334 333 # TODO(marcink): deprecate GET at some point
335 334 if request.method not in ['POST', 'GET']:
336 335 log.debug('unsupported request method "%s"', request.method)
337 336 raise JSONRPCError(
338 337 'unsupported request method "%s". Please use POST' % request.method)
339 338
340 339 if 'CONTENT_LENGTH' not in request.environ:
341 340 log.debug("No Content-Length")
342 341 raise JSONRPCError("Empty body, No Content-Length in request")
343 342
344 343 else:
345 344 length = request.environ['CONTENT_LENGTH']
346 345 log.debug('Content-Length: %s', length)
347 346
348 347 if length == 0:
349 348 log.debug("Content-Length is 0")
350 349 raise JSONRPCError("Content-Length is 0")
351 350
352 351 raw_body = request.body
353 352 log.debug("Loading JSON body now")
354 353 try:
355 354 json_body = ext_json.json.loads(raw_body)
356 355 except ValueError as e:
357 356 # catch JSON errors Here
358 357 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
359 358
360 359 request.rpc_id = json_body.get('id')
361 360 request.rpc_method = json_body.get('method')
362 361
363 362 # check required base parameters
364 363 try:
365 364 api_key = json_body.get('api_key')
366 365 if not api_key:
367 366 api_key = json_body.get('auth_token')
368 367
369 368 if not api_key:
370 369 raise KeyError('api_key or auth_token')
371 370
372 371 # TODO(marcink): support passing in token in request header
373 372
374 373 request.rpc_api_key = api_key
375 374 request.rpc_id = json_body['id']
376 375 request.rpc_method = json_body['method']
377 376 request.rpc_params = json_body['args'] \
378 377 if isinstance(json_body['args'], dict) else {}
379 378
380 379 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
381 380 except KeyError as e:
382 381 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
383 382
384 383 log.debug('setup complete, now handling method:%s rpcid:%s',
385 384 request.rpc_method, request.rpc_id, )
386 385
387 386
388 387 class RoutePredicate(object):
389 388 def __init__(self, val, config):
390 389 self.val = val
391 390
392 391 def text(self):
393 392 return f'jsonrpc route = {self.val}'
394 393
395 394 phash = text
396 395
397 396 def __call__(self, info, request):
398 397 if self.val:
399 398 # potentially setup and bootstrap our call
400 399 setup_request(request)
401 400
402 401 # Always return True so that even if it isn't a valid RPC it
403 402 # will fall through to the underlaying handlers like notfound_view
404 403 return True
405 404
406 405
407 406 class NotFoundPredicate(object):
408 407 def __init__(self, val, config):
409 408 self.val = val
410 409 self.methods = config.registry.jsonrpc_methods
411 410
412 411 def text(self):
413 412 return f'jsonrpc method not found = {self.val}'
414 413
415 414 phash = text
416 415
417 416 def __call__(self, info, request):
418 417 return hasattr(request, 'rpc_method')
419 418
420 419
421 420 class MethodPredicate(object):
422 421 def __init__(self, val, config):
423 422 self.method = val
424 423
425 424 def text(self):
426 425 return f'jsonrpc method = {self.method}'
427 426
428 427 phash = text
429 428
430 429 def __call__(self, context, request):
431 430 # we need to explicitly return False here, so pyramid doesn't try to
432 431 # execute our view directly. We need our main handler to execute things
433 432 return getattr(request, 'rpc_method') == self.method
434 433
435 434
436 435 def add_jsonrpc_method(config, view, **kwargs):
437 436 # pop the method name
438 437 method = kwargs.pop('method', None)
439 438
440 439 if method is None:
441 440 raise ConfigurationError(
442 441 'Cannot register a JSON-RPC method without specifying the "method"')
443 442
444 443 # we define custom predicate, to enable to detect conflicting methods,
445 444 # those predicates are kind of "translation" from the decorator variables
446 445 # to internal predicates names
447 446
448 447 kwargs['jsonrpc_method'] = method
449 448
450 449 # register our view into global view store for validation
451 450 config.registry.jsonrpc_methods[method] = view
452 451
453 452 # we're using our main request_view handler, here, so each method
454 453 # has a unified handler for itself
455 454 config.add_view(request_view, route_name='apiv2', **kwargs)
456 455
457 456
458 457 class jsonrpc_method(object):
459 458 """
460 459 decorator that works similar to @add_view_config decorator,
461 460 but tailored for our JSON RPC
462 461 """
463 462
464 463 venusian = venusian # for testing injection
465 464
466 465 def __init__(self, method=None, **kwargs):
467 466 self.method = method
468 467 self.kwargs = kwargs
469 468
470 469 def __call__(self, wrapped):
471 470 kwargs = self.kwargs.copy()
472 471 kwargs['method'] = self.method or wrapped.__name__
473 472 depth = kwargs.pop('_depth', 0)
474 473
475 474 def callback(context, name, ob):
476 475 config = context.config.with_package(info.module)
477 476 config.add_jsonrpc_method(view=ob, **kwargs)
478 477
479 478 info = venusian.attach(wrapped, callback, category='pyramid',
480 479 depth=depth + 1)
481 480 if info.scope == 'class':
482 481 # ensure that attr is set if decorating a class method
483 482 kwargs.setdefault('attr', wrapped.__name__)
484 483
485 484 kwargs['_info'] = info.codeinfo # fbo action_method
486 485 return wrapped
487 486
488 487
489 488 class jsonrpc_deprecated_method(object):
490 489 """
491 490 Marks method as deprecated, adds log.warning, and inject special key to
492 491 the request variable to mark method as deprecated.
493 492 Also injects special docstring that extract_docs will catch to mark
494 493 method as deprecated.
495 494
496 495 :param use_method: specify which method should be used instead of
497 496 the decorated one
498 497
499 498 Use like::
500 499
501 500 @jsonrpc_method()
502 501 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
503 502 def old_func(request, apiuser, arg1, arg2):
504 503 ...
505 504 """
506 505
507 506 def __init__(self, use_method, deprecated_at_version):
508 507 self.use_method = use_method
509 508 self.deprecated_at_version = deprecated_at_version
510 509 self.deprecated_msg = ''
511 510
512 511 def __call__(self, func):
513 512 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
514 513 method=self.use_method)
515 514
516 515 docstring = """\n
517 516 .. deprecated:: {version}
518 517
519 518 {deprecation_message}
520 519
521 520 {original_docstring}
522 521 """
523 522 func.__doc__ = docstring.format(
524 523 version=self.deprecated_at_version,
525 524 deprecation_message=self.deprecated_msg,
526 525 original_docstring=func.__doc__)
527 526 return decorator.decorator(self.__wrapper, func)
528 527
529 528 def __wrapper(self, func, *fargs, **fkwargs):
530 529 log.warning('DEPRECATED API CALL on function %s, please '
531 530 'use `%s` instead', func, self.use_method)
532 531 # alter function docstring to mark as deprecated, this is picked up
533 532 # via fabric file that generates API DOC.
534 533 result = func(*fargs, **fkwargs)
535 534
536 535 request = fargs[0]
537 536 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
538 537 return result
539 538
540 539
541 540 def add_api_methods(config):
542 541 from rhodecode.api.views import (
543 542 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
544 543 server_api, search_api, testing_api, user_api, user_group_api)
545 544
546 545 config.scan('rhodecode.api.views')
547 546
548 547
549 548 def includeme(config):
550 549 plugin_module = 'rhodecode.api'
551 550 plugin_settings = get_plugin_settings(
552 551 plugin_module, config.registry.settings)
553 552
554 553 if not hasattr(config.registry, 'jsonrpc_methods'):
555 554 config.registry.jsonrpc_methods = OrderedDict()
556 555
557 556 # match filter by given method only
558 557 config.add_view_predicate('jsonrpc_method', MethodPredicate)
559 558 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
560 559
561 560 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
562 561 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
563 562
564 563 config.add_route_predicate(
565 564 'jsonrpc_call', RoutePredicate)
566 565
567 566 config.add_route(
568 567 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
569 568
570 569 # register some exception handling view
571 570 config.add_view(exception_view, context=JSONRPCBaseError)
572 571 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
573 572
574 573 add_api_methods(config)
@@ -1,172 +1,156 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21 import csv
22 22 import datetime
23 23
24 24 import pytest
25 25
26 26 from rhodecode.lib.str_utils import safe_str
27 27 from rhodecode.tests import *
28 from rhodecode.tests.routes import route_path
28 29 from rhodecode.tests.fixture import FIXTURES
29 30 from rhodecode.model.db import UserLog
30 31 from rhodecode.model.meta import Session
31 32
32 33
33 def route_path(name, params=None, **kwargs):
34 import urllib.request
35 import urllib.parse
36 import urllib.error
37 from rhodecode.apps._base import ADMIN_PREFIX
38
39 base_url = {
40 'admin_home': ADMIN_PREFIX,
41 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
42
43 }[name].format(**kwargs)
44
45 if params:
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 return base_url
48
49
50 34 @pytest.mark.usefixtures('app')
51 35 class TestAdminController(object):
52 36
53 37 @pytest.fixture(scope='class', autouse=True)
54 38 def prepare(self, request, baseapp):
55 39 UserLog.query().delete()
56 40 Session().commit()
57 41
58 42 def strptime(val):
59 43 fmt = '%Y-%m-%d %H:%M:%S'
60 44 if '.' not in val:
61 45 return datetime.datetime.strptime(val, fmt)
62 46
63 47 nofrag, frag = val.split(".")
64 48 date = datetime.datetime.strptime(nofrag, fmt)
65 49
66 50 frag = frag[:6] # truncate to microseconds
67 51 frag += (6 - len(frag)) * '0' # add 0s
68 52 return date.replace(microsecond=int(frag))
69 53
70 54 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
71 55 for row in csv.DictReader(f):
72 56 ul = UserLog()
73 57 for k, v in row.items():
74 58 v = safe_str(v)
75 59 if k == 'action_date':
76 60 v = strptime(v)
77 61 if k in ['user_id', 'repository_id']:
78 62 # nullable due to FK problems
79 63 v = None
80 64 setattr(ul, k, v)
81 65 Session().add(ul)
82 66 Session().commit()
83 67
84 68 @request.addfinalizer
85 69 def cleanup():
86 70 UserLog.query().delete()
87 71 Session().commit()
88 72
89 73 def test_index(self, autologin_user):
90 74 response = self.app.get(route_path('admin_audit_logs'))
91 75 response.mustcontain('Admin audit logs')
92 76
93 77 def test_filter_all_entries(self, autologin_user):
94 78 response = self.app.get(route_path('admin_audit_logs'))
95 79 all_count = UserLog.query().count()
96 80 response.mustcontain('%s entries' % all_count)
97 81
98 82 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
99 83 response = self.app.get(route_path('admin_audit_logs',
100 84 params=dict(filter='repository:rhodecode')))
101 85 response.mustcontain('3 entries')
102 86
103 87 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
104 88 response = self.app.get(route_path('admin_audit_logs',
105 89 params=dict(filter='repository:RhodeCode')))
106 90 response.mustcontain('3 entries')
107 91
108 92 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
109 93 response = self.app.get(route_path('admin_audit_logs',
110 94 params=dict(filter='repository:*test*')))
111 95 response.mustcontain('862 entries')
112 96
113 97 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
114 98 response = self.app.get(route_path('admin_audit_logs',
115 99 params=dict(filter='repository:test*')))
116 100 response.mustcontain('257 entries')
117 101
118 102 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
119 103 response = self.app.get(route_path('admin_audit_logs',
120 104 params=dict(filter='repository:Test*')))
121 105 response.mustcontain('257 entries')
122 106
123 107 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
124 108 response = self.app.get(route_path('admin_audit_logs',
125 109 params=dict(filter='repository:test* AND username:demo')))
126 110 response.mustcontain('130 entries')
127 111
128 112 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
129 113 response = self.app.get(route_path('admin_audit_logs',
130 114 params=dict(filter='repository:test* OR repository:rhodecode')))
131 115 response.mustcontain('260 entries') # 257 + 3
132 116
133 117 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
134 118 response = self.app.get(route_path('admin_audit_logs',
135 119 params=dict(filter='username:demo')))
136 120 response.mustcontain('1087 entries')
137 121
138 122 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
139 123 response = self.app.get(route_path('admin_audit_logs',
140 124 params=dict(filter='username:DemO')))
141 125 response.mustcontain('1087 entries')
142 126
143 127 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
144 128 response = self.app.get(route_path('admin_audit_logs',
145 129 params=dict(filter='username:*test*')))
146 130 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
147 131 response.mustcontain('{} entries'.format(entries_count))
148 132
149 133 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
150 134 response = self.app.get(route_path('admin_audit_logs',
151 135 params=dict(filter='username:demo*')))
152 136 response.mustcontain('1101 entries')
153 137
154 138 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
155 139 response = self.app.get(route_path('admin_audit_logs',
156 140 params=dict(filter='username:demo OR username:volcan')))
157 141 response.mustcontain('1095 entries') # 1087 + 8
158 142
159 143 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
160 144 response = self.app.get(route_path('admin_audit_logs',
161 145 params=dict(filter='action:*pull_request*')))
162 146 response.mustcontain('187 entries')
163 147
164 148 def test_filter_journal_filter_on_date(self, autologin_user):
165 149 response = self.app.get(route_path('admin_audit_logs',
166 150 params=dict(filter='date:20121010')))
167 151 response.mustcontain('47 entries')
168 152
169 153 def test_filter_journal_filter_on_date_2(self, autologin_user):
170 154 response = self.app.get(route_path('admin_audit_logs',
171 155 params=dict(filter='date:20121020')))
172 156 response.mustcontain('17 entries')
@@ -1,86 +1,69 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import assert_session_flash
23 from rhodecode.tests.routes import route_path
23 24 from rhodecode.model.settings import SettingsModel
24 25
25 26
26 def route_path(name, params=None, **kwargs):
27 import urllib.request
28 import urllib.parse
29 import urllib.error
30 from rhodecode.apps._base import ADMIN_PREFIX
31
32 base_url = {
33 'admin_defaults_repositories':
34 ADMIN_PREFIX + '/defaults/repositories',
35 'admin_defaults_repositories_update':
36 ADMIN_PREFIX + '/defaults/repositories/update',
37 }[name].format(**kwargs)
38
39 if params:
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
42
43
44 27 @pytest.mark.usefixtures("app")
45 28 class TestDefaultsView(object):
46 29
47 30 def test_index(self, autologin_user):
48 31 response = self.app.get(route_path('admin_defaults_repositories'))
49 32 response.mustcontain('default_repo_private')
50 33 response.mustcontain('default_repo_enable_statistics')
51 34 response.mustcontain('default_repo_enable_downloads')
52 35 response.mustcontain('default_repo_enable_locking')
53 36
54 37 def test_update_params_true_hg(self, autologin_user, csrf_token):
55 38 params = {
56 39 'default_repo_enable_locking': True,
57 40 'default_repo_enable_downloads': True,
58 41 'default_repo_enable_statistics': True,
59 42 'default_repo_private': True,
60 43 'default_repo_type': 'hg',
61 44 'csrf_token': csrf_token,
62 45 }
63 46 response = self.app.post(
64 47 route_path('admin_defaults_repositories_update'), params=params)
65 48 assert_session_flash(response, 'Default settings updated successfully')
66 49
67 50 defs = SettingsModel().get_default_repo_settings()
68 51 del params['csrf_token']
69 52 assert params == defs
70 53
71 54 def test_update_params_false_git(self, autologin_user, csrf_token):
72 55 params = {
73 56 'default_repo_enable_locking': False,
74 57 'default_repo_enable_downloads': False,
75 58 'default_repo_enable_statistics': False,
76 59 'default_repo_private': False,
77 60 'default_repo_type': 'git',
78 61 'csrf_token': csrf_token,
79 62 }
80 63 response = self.app.post(
81 64 route_path('admin_defaults_repositories_update'), params=params)
82 65 assert_session_flash(response, 'Default settings updated successfully')
83 66
84 67 defs = SettingsModel().get_default_repo_settings()
85 68 del params['csrf_token']
86 69 assert params == defs
@@ -1,86 +1,67 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import TestController
23 23 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.routes import route_path
24 25
25 26 fixture = Fixture()
26 27
27 28
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
32 from rhodecode.apps._base import ADMIN_PREFIX
33
34 base_url = {
35 'admin_home': ADMIN_PREFIX,
36 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
37 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
38 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
39 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
40
41 }[name].format(**kwargs)
42
43 if params:
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 return base_url
46
47
48 29 class TestAdminMainView(TestController):
49 30
50 31 def test_access_admin_home(self):
51 32 self.log_user()
52 33 response = self.app.get(route_path('admin_home'), status=200)
53 34 response.mustcontain("Administration area")
54 35
55 36 @pytest.mark.parametrize('view', [
56 37 'pull_requests_global',
57 38 ])
58 39 def test_redirect_pull_request_view_global(self, view):
59 40 self.log_user()
60 41 self.app.get(
61 42 route_path(view, pull_request_id='xxxx'),
62 43 status=404)
63 44
64 45 @pytest.mark.backends("git", "hg")
65 46 @pytest.mark.parametrize('view', [
66 47 'pull_requests_global',
67 48 'pull_requests_global_0',
68 49 'pull_requests_global_1',
69 50 ])
70 51 def test_redirect_pull_request_view(self, view, pr_util):
71 52 self.log_user()
72 53 pull_request = pr_util.create_pull_request()
73 54 pull_request_id = pull_request.pull_request_id
74 55 repo_name = pull_request.target_repo.repo_name
75 56
76 57 response = self.app.get(
77 58 route_path(view, pull_request_id=pull_request_id),
78 59 status=302)
79 60 assert response.location.endswith(
80 61 'pull-request/{}'.format(pull_request_id))
81 62
82 63 redirect_url = route_path(
83 64 'pullrequest_show', repo_name=repo_name,
84 65 pull_request_id=pull_request_id)
85 66
86 67 assert redirect_url in response.location
@@ -1,300 +1,253 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22 from rhodecode.model.db import User, UserIpMap
23 23 from rhodecode.model.meta import Session
24 24 from rhodecode.model.permission import PermissionModel
25 25 from rhodecode.model.ssh_key import SshKeyModel
26 26 from rhodecode.tests import (
27 27 TestController, clear_cache_regions, assert_session_flash)
28
29
30 def route_path(name, params=None, **kwargs):
31 import urllib.request
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'edit_user_ips':
38 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
39 'edit_user_ips_add':
40 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
41 'edit_user_ips_delete':
42 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
43
44 'admin_permissions_application':
45 ADMIN_PREFIX + '/permissions/application',
46 'admin_permissions_application_update':
47 ADMIN_PREFIX + '/permissions/application/update',
48
49 'admin_permissions_global':
50 ADMIN_PREFIX + '/permissions/global',
51 'admin_permissions_global_update':
52 ADMIN_PREFIX + '/permissions/global/update',
53
54 'admin_permissions_object':
55 ADMIN_PREFIX + '/permissions/object',
56 'admin_permissions_object_update':
57 ADMIN_PREFIX + '/permissions/object/update',
58
59 'admin_permissions_ips':
60 ADMIN_PREFIX + '/permissions/ips',
61 'admin_permissions_overview':
62 ADMIN_PREFIX + '/permissions/overview',
63
64 'admin_permissions_ssh_keys':
65 ADMIN_PREFIX + '/permissions/ssh_keys',
66 'admin_permissions_ssh_keys_data':
67 ADMIN_PREFIX + '/permissions/ssh_keys/data',
68 'admin_permissions_ssh_keys_update':
69 ADMIN_PREFIX + '/permissions/ssh_keys/update'
70
71 }[name].format(**kwargs)
72
73 if params:
74 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
75 return base_url
28 from rhodecode.tests.routes import route_path
76 29
77 30
78 31 class TestAdminPermissionsController(TestController):
79 32
80 33 @pytest.fixture(scope='class', autouse=True)
81 34 def prepare(self, request):
82 35 # cleanup and reset to default permissions after
83 36 @request.addfinalizer
84 37 def cleanup():
85 38 PermissionModel().create_default_user_permissions(
86 39 User.get_default_user(), force=True)
87 40
88 41 def test_index_application(self):
89 42 self.log_user()
90 43 self.app.get(route_path('admin_permissions_application'))
91 44
92 45 @pytest.mark.parametrize(
93 46 'anonymous, default_register, default_register_message, default_password_reset,'
94 47 'default_extern_activate, expect_error, expect_form_error', [
95 48 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
96 49 False, False),
97 50 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
98 51 False, False),
99 52 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
100 53 False, False),
101 54 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
102 55 False, False),
103 56 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
104 57 False, True),
105 58 (True, '', '', 'hg.password_reset.enabled', '', True, False),
106 59 ])
107 60 def test_update_application_permissions(
108 61 self, anonymous, default_register, default_register_message, default_password_reset,
109 62 default_extern_activate, expect_error, expect_form_error):
110 63
111 64 self.log_user()
112 65
113 66 # TODO: anonymous access set here to False, breaks some other tests
114 67 params = {
115 68 'csrf_token': self.csrf_token,
116 69 'anonymous': anonymous,
117 70 'default_register': default_register,
118 71 'default_register_message': default_register_message,
119 72 'default_password_reset': default_password_reset,
120 73 'default_extern_activate': default_extern_activate,
121 74 }
122 75 response = self.app.post(route_path('admin_permissions_application_update'),
123 76 params=params)
124 77 if expect_form_error:
125 78 assert response.status_int == 200
126 79 response.mustcontain('Value must be one of')
127 80 else:
128 81 if expect_error:
129 82 msg = 'Error occurred during update of permissions'
130 83 else:
131 84 msg = 'Application permissions updated successfully'
132 85 assert_session_flash(response, msg)
133 86
134 87 def test_index_object(self):
135 88 self.log_user()
136 89 self.app.get(route_path('admin_permissions_object'))
137 90
138 91 @pytest.mark.parametrize(
139 92 'repo, repo_group, user_group, expect_error, expect_form_error', [
140 93 ('repository.none', 'group.none', 'usergroup.none', False, False),
141 94 ('repository.read', 'group.read', 'usergroup.read', False, False),
142 95 ('repository.write', 'group.write', 'usergroup.write',
143 96 False, False),
144 97 ('repository.admin', 'group.admin', 'usergroup.admin',
145 98 False, False),
146 99 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
147 100 ('', '', '', True, False),
148 101 ])
149 102 def test_update_object_permissions(self, repo, repo_group, user_group,
150 103 expect_error, expect_form_error):
151 104 self.log_user()
152 105
153 106 params = {
154 107 'csrf_token': self.csrf_token,
155 108 'default_repo_perm': repo,
156 109 'overwrite_default_repo': False,
157 110 'default_group_perm': repo_group,
158 111 'overwrite_default_group': False,
159 112 'default_user_group_perm': user_group,
160 113 'overwrite_default_user_group': False,
161 114 }
162 115 response = self.app.post(route_path('admin_permissions_object_update'),
163 116 params=params)
164 117 if expect_form_error:
165 118 assert response.status_int == 200
166 119 response.mustcontain('Value must be one of')
167 120 else:
168 121 if expect_error:
169 122 msg = 'Error occurred during update of permissions'
170 123 else:
171 124 msg = 'Object permissions updated successfully'
172 125 assert_session_flash(response, msg)
173 126
174 127 def test_index_global(self):
175 128 self.log_user()
176 129 self.app.get(route_path('admin_permissions_global'))
177 130
178 131 @pytest.mark.parametrize(
179 132 'repo_create, repo_create_write, user_group_create, repo_group_create,'
180 133 'fork_create, inherit_default_permissions, expect_error,'
181 134 'expect_form_error', [
182 135 ('hg.create.none', 'hg.create.write_on_repogroup.false',
183 136 'hg.usergroup.create.false', 'hg.repogroup.create.false',
184 137 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
185 138 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
186 139 'hg.usergroup.create.true', 'hg.repogroup.create.true',
187 140 'hg.fork.repository', 'hg.inherit_default_perms.false',
188 141 False, False),
189 142 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
190 143 'hg.usergroup.create.true', 'hg.repogroup.create.true',
191 144 'hg.fork.repository', 'hg.inherit_default_perms.false',
192 145 False, True),
193 146 ('', '', '', '', '', '', True, False),
194 147 ])
195 148 def test_update_global_permissions(
196 149 self, repo_create, repo_create_write, user_group_create,
197 150 repo_group_create, fork_create, inherit_default_permissions,
198 151 expect_error, expect_form_error):
199 152 self.log_user()
200 153
201 154 params = {
202 155 'csrf_token': self.csrf_token,
203 156 'default_repo_create': repo_create,
204 157 'default_repo_create_on_write': repo_create_write,
205 158 'default_user_group_create': user_group_create,
206 159 'default_repo_group_create': repo_group_create,
207 160 'default_fork_create': fork_create,
208 161 'default_inherit_default_permissions': inherit_default_permissions
209 162 }
210 163 response = self.app.post(route_path('admin_permissions_global_update'),
211 164 params=params)
212 165 if expect_form_error:
213 166 assert response.status_int == 200
214 167 response.mustcontain('Value must be one of')
215 168 else:
216 169 if expect_error:
217 170 msg = 'Error occurred during update of permissions'
218 171 else:
219 172 msg = 'Global permissions updated successfully'
220 173 assert_session_flash(response, msg)
221 174
222 175 def test_index_ips(self):
223 176 self.log_user()
224 177 response = self.app.get(route_path('admin_permissions_ips'))
225 178 response.mustcontain('All IP addresses are allowed')
226 179
227 180 def test_add_delete_ips(self):
228 181 clear_cache_regions(['sql_cache_short'])
229 182 self.log_user()
230 183
231 184 # ADD
232 185 default_user_id = User.get_default_user_id()
233 186 self.app.post(
234 187 route_path('edit_user_ips_add', user_id=default_user_id),
235 188 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
236 189
237 190 response = self.app.get(route_path('admin_permissions_ips'))
238 191 response.mustcontain('0.0.0.0/24')
239 192 response.mustcontain('0.0.0.0 - 0.0.0.255')
240 193
241 194 # DELETE
242 195 default_user_id = User.get_default_user_id()
243 196 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
244 197 default_user_id).first().ip_id
245 198
246 199 response = self.app.post(
247 200 route_path('edit_user_ips_delete', user_id=default_user_id),
248 201 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
249 202
250 203 assert_session_flash(response, 'Removed ip address from user whitelist')
251 204
252 205 clear_cache_regions(['sql_cache_short'])
253 206 response = self.app.get(route_path('admin_permissions_ips'))
254 207 response.mustcontain('All IP addresses are allowed')
255 208 response.mustcontain(no=['0.0.0.0/24'])
256 209 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
257 210
258 211 def test_index_overview(self):
259 212 self.log_user()
260 213 self.app.get(route_path('admin_permissions_overview'))
261 214
262 215 def test_ssh_keys(self):
263 216 self.log_user()
264 217 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
265 218
266 219 def test_ssh_keys_data(self, user_util, xhr_header):
267 220 self.log_user()
268 221 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
269 222 extra_environ=xhr_header)
270 223 assert response.json == {u'data': [], u'draw': None,
271 224 u'recordsFiltered': 0, u'recordsTotal': 0}
272 225
273 226 dummy_user = user_util.create_user()
274 227 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
275 228 Session().commit()
276 229 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
277 230 extra_environ=xhr_header)
278 231 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
279 232
280 233 def test_ssh_keys_update(self):
281 234 self.log_user()
282 235 response = self.app.post(
283 236 route_path('admin_permissions_ssh_keys_update'),
284 237 dict(csrf_token=self.csrf_token), status=302)
285 238
286 239 assert_session_flash(
287 240 response, 'Updated SSH keys file')
288 241
289 242 def test_ssh_keys_update_disabled(self):
290 243 self.log_user()
291 244
292 245 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
293 246 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
294 247 return_value=False):
295 248 response = self.app.post(
296 249 route_path('admin_permissions_ssh_keys_update'),
297 250 dict(csrf_token=self.csrf_token), status=302)
298 251
299 252 assert_session_flash(
300 253 response, 'SSH key support is disabled in .ini file') No newline at end of file
@@ -1,515 +1,497 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import urllib.request
21 21 import urllib.parse
22 22 import urllib.error
23 23
24 24 import mock
25 25 import pytest
26 26
27 27 from rhodecode.apps._base import ADMIN_PREFIX
28 28 from rhodecode.lib import auth
29 29 from rhodecode.lib.utils2 import safe_str
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.model.db import (
32 32 Repository, RepoGroup, UserRepoToPerm, User, Permission)
33 33 from rhodecode.model.meta import Session
34 34 from rhodecode.model.repo import RepoModel
35 35 from rhodecode.model.repo_group import RepoGroupModel
36 36 from rhodecode.model.user import UserModel
37 37 from rhodecode.tests import (
38 38 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
39 39 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
40 40 from rhodecode.tests.fixture import Fixture, error_function
41 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
41 from rhodecode.tests.utils import repo_on_filesystem
42 from rhodecode.tests.routes import route_path
42 43
43 44 fixture = Fixture()
44 45
45 46
46 def route_path(name, params=None, **kwargs):
47 import urllib.request
48 import urllib.parse
49 import urllib.error
50
51 base_url = {
52 'repos': ADMIN_PREFIX + '/repos',
53 'repos_data': ADMIN_PREFIX + '/repos_data',
54 'repo_new': ADMIN_PREFIX + '/repos/new',
55 'repo_create': ADMIN_PREFIX + '/repos/create',
56
57 'repo_creating_check': '/{repo_name}/repo_creating_check',
58 }[name].format(**kwargs)
59
60 if params:
61 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
62 return base_url
63
64
65 47 def _get_permission_for_user(user, repo):
66 48 perm = UserRepoToPerm.query()\
67 49 .filter(UserRepoToPerm.repository ==
68 50 Repository.get_by_repo_name(repo))\
69 51 .filter(UserRepoToPerm.user == User.get_by_username(user))\
70 52 .all()
71 53 return perm
72 54
73 55
74 56 @pytest.mark.usefixtures("app")
75 57 class TestAdminRepos(object):
76 58
77 59 def test_repo_list(self, autologin_user, user_util, xhr_header):
78 60 repo = user_util.create_repo()
79 61 repo_name = repo.repo_name
80 62 response = self.app.get(
81 63 route_path('repos_data'), status=200,
82 64 extra_environ=xhr_header)
83 65
84 66 response.mustcontain(repo_name)
85 67
86 68 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
87 69 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
88 70 response = self.app.get(route_path('repo_new'), status=200)
89 71 assert_response = response.assert_response()
90 72 element = assert_response.get_element('[name=repo_type]')
91 73 assert element.get('value') == 'git'
92 74
93 75 def test_create_page_non_restricted_backends(self, autologin_user, backend):
94 76 response = self.app.get(route_path('repo_new'), status=200)
95 77 assert_response = response.assert_response()
96 78 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
97 79
98 80 @pytest.mark.parametrize(
99 81 "suffix", ['', 'xxa'], ids=['', 'non-ascii'])
100 82 def test_create(self, autologin_user, backend, suffix, csrf_token):
101 83 repo_name_unicode = backend.new_repo_name(suffix=suffix)
102 84 repo_name = repo_name_unicode
103 85
104 86 description_unicode = 'description for newly created repo' + suffix
105 87 description = description_unicode
106 88
107 89 response = self.app.post(
108 90 route_path('repo_create'),
109 91 fixture._get_repo_create_params(
110 92 repo_private=False,
111 93 repo_name=repo_name,
112 94 repo_type=backend.alias,
113 95 repo_description=description,
114 96 csrf_token=csrf_token),
115 97 status=302)
116 98
117 99 self.assert_repository_is_created_correctly(
118 100 repo_name, description, backend)
119 101
120 102 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
121 103 numeric_repo = '1234'
122 104 repo_name = numeric_repo
123 105 description = 'description for newly created repo' + numeric_repo
124 106 self.app.post(
125 107 route_path('repo_create'),
126 108 fixture._get_repo_create_params(
127 109 repo_private=False,
128 110 repo_name=repo_name,
129 111 repo_type=backend.alias,
130 112 repo_description=description,
131 113 csrf_token=csrf_token))
132 114
133 115 self.assert_repository_is_created_correctly(
134 116 repo_name, description, backend)
135 117
136 118 @pytest.mark.parametrize("suffix", ['', '_ąćę'], ids=['', 'non-ascii'])
137 119 def test_create_in_group(
138 120 self, autologin_user, backend, suffix, csrf_token):
139 121 # create GROUP
140 122 group_name = f'sometest_{backend.alias}'
141 123 gr = RepoGroupModel().create(group_name=group_name,
142 124 group_description='test',
143 125 owner=TEST_USER_ADMIN_LOGIN)
144 126 Session().commit()
145 127
146 128 repo_name = f'ingroup{suffix}'
147 129 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
148 130 description = 'description for newly created repo'
149 131
150 132 self.app.post(
151 133 route_path('repo_create'),
152 134 fixture._get_repo_create_params(
153 135 repo_private=False,
154 136 repo_name=safe_str(repo_name),
155 137 repo_type=backend.alias,
156 138 repo_description=description,
157 139 repo_group=gr.group_id,
158 140 csrf_token=csrf_token))
159 141
160 142 # TODO: johbo: Cleanup work to fixture
161 143 try:
162 144 self.assert_repository_is_created_correctly(
163 145 repo_name_full, description, backend)
164 146
165 147 new_repo = RepoModel().get_by_repo_name(repo_name_full)
166 148 inherited_perms = UserRepoToPerm.query().filter(
167 149 UserRepoToPerm.repository_id == new_repo.repo_id).all()
168 150 assert len(inherited_perms) == 1
169 151 finally:
170 152 RepoModel().delete(repo_name_full)
171 153 RepoGroupModel().delete(group_name)
172 154 Session().commit()
173 155
174 156 def test_create_in_group_numeric_name(
175 157 self, autologin_user, backend, csrf_token):
176 158 # create GROUP
177 159 group_name = 'sometest_%s' % backend.alias
178 160 gr = RepoGroupModel().create(group_name=group_name,
179 161 group_description='test',
180 162 owner=TEST_USER_ADMIN_LOGIN)
181 163 Session().commit()
182 164
183 165 repo_name = '12345'
184 166 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
185 167 description = 'description for newly created repo'
186 168 self.app.post(
187 169 route_path('repo_create'),
188 170 fixture._get_repo_create_params(
189 171 repo_private=False,
190 172 repo_name=repo_name,
191 173 repo_type=backend.alias,
192 174 repo_description=description,
193 175 repo_group=gr.group_id,
194 176 csrf_token=csrf_token))
195 177
196 178 # TODO: johbo: Cleanup work to fixture
197 179 try:
198 180 self.assert_repository_is_created_correctly(
199 181 repo_name_full, description, backend)
200 182
201 183 new_repo = RepoModel().get_by_repo_name(repo_name_full)
202 184 inherited_perms = UserRepoToPerm.query()\
203 185 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
204 186 assert len(inherited_perms) == 1
205 187 finally:
206 188 RepoModel().delete(repo_name_full)
207 189 RepoGroupModel().delete(group_name)
208 190 Session().commit()
209 191
210 192 def test_create_in_group_without_needed_permissions(self, backend):
211 193 session = login_user_session(
212 194 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
213 195 csrf_token = auth.get_csrf_token(session)
214 196 # revoke
215 197 user_model = UserModel()
216 198 # disable fork and create on default user
217 199 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
218 200 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
219 201 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
220 202 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
221 203
222 204 # disable on regular user
223 205 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
224 206 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
225 207 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
226 208 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
227 209 Session().commit()
228 210
229 211 # create GROUP
230 212 group_name = 'reg_sometest_%s' % backend.alias
231 213 gr = RepoGroupModel().create(group_name=group_name,
232 214 group_description='test',
233 215 owner=TEST_USER_ADMIN_LOGIN)
234 216 Session().commit()
235 217 repo_group_id = gr.group_id
236 218
237 219 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
238 220 gr_allowed = RepoGroupModel().create(
239 221 group_name=group_name_allowed,
240 222 group_description='test',
241 223 owner=TEST_USER_REGULAR_LOGIN)
242 224 allowed_repo_group_id = gr_allowed.group_id
243 225 Session().commit()
244 226
245 227 repo_name = 'ingroup'
246 228 description = 'description for newly created repo'
247 229 response = self.app.post(
248 230 route_path('repo_create'),
249 231 fixture._get_repo_create_params(
250 232 repo_private=False,
251 233 repo_name=repo_name,
252 234 repo_type=backend.alias,
253 235 repo_description=description,
254 236 repo_group=repo_group_id,
255 237 csrf_token=csrf_token))
256 238
257 239 response.mustcontain('Invalid value')
258 240
259 241 # user is allowed to create in this group
260 242 repo_name = 'ingroup'
261 243 repo_name_full = RepoGroup.url_sep().join(
262 244 [group_name_allowed, repo_name])
263 245 description = 'description for newly created repo'
264 246 response = self.app.post(
265 247 route_path('repo_create'),
266 248 fixture._get_repo_create_params(
267 249 repo_private=False,
268 250 repo_name=repo_name,
269 251 repo_type=backend.alias,
270 252 repo_description=description,
271 253 repo_group=allowed_repo_group_id,
272 254 csrf_token=csrf_token))
273 255
274 256 # TODO: johbo: Cleanup in pytest fixture
275 257 try:
276 258 self.assert_repository_is_created_correctly(
277 259 repo_name_full, description, backend)
278 260
279 261 new_repo = RepoModel().get_by_repo_name(repo_name_full)
280 262 inherited_perms = UserRepoToPerm.query().filter(
281 263 UserRepoToPerm.repository_id == new_repo.repo_id).all()
282 264 assert len(inherited_perms) == 1
283 265
284 266 assert repo_on_filesystem(repo_name_full)
285 267 finally:
286 268 RepoModel().delete(repo_name_full)
287 269 RepoGroupModel().delete(group_name)
288 270 RepoGroupModel().delete(group_name_allowed)
289 271 Session().commit()
290 272
291 273 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
292 274 csrf_token):
293 275 # create GROUP
294 276 group_name = 'sometest_%s' % backend.alias
295 277 gr = RepoGroupModel().create(group_name=group_name,
296 278 group_description='test',
297 279 owner=TEST_USER_ADMIN_LOGIN)
298 280 perm = Permission.get_by_key('repository.write')
299 281 RepoGroupModel().grant_user_permission(
300 282 gr, TEST_USER_REGULAR_LOGIN, perm)
301 283
302 284 # add repo permissions
303 285 Session().commit()
304 286 repo_group_id = gr.group_id
305 287 repo_name = 'ingroup_inherited_%s' % backend.alias
306 288 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
307 289 description = 'description for newly created repo'
308 290 self.app.post(
309 291 route_path('repo_create'),
310 292 fixture._get_repo_create_params(
311 293 repo_private=False,
312 294 repo_name=repo_name,
313 295 repo_type=backend.alias,
314 296 repo_description=description,
315 297 repo_group=repo_group_id,
316 298 repo_copy_permissions=True,
317 299 csrf_token=csrf_token))
318 300
319 301 # TODO: johbo: Cleanup to pytest fixture
320 302 try:
321 303 self.assert_repository_is_created_correctly(
322 304 repo_name_full, description, backend)
323 305 except Exception:
324 306 RepoGroupModel().delete(group_name)
325 307 Session().commit()
326 308 raise
327 309
328 310 # check if inherited permissions are applied
329 311 new_repo = RepoModel().get_by_repo_name(repo_name_full)
330 312 inherited_perms = UserRepoToPerm.query().filter(
331 313 UserRepoToPerm.repository_id == new_repo.repo_id).all()
332 314 assert len(inherited_perms) == 2
333 315
334 316 assert TEST_USER_REGULAR_LOGIN in [
335 317 x.user.username for x in inherited_perms]
336 318 assert 'repository.write' in [
337 319 x.permission.permission_name for x in inherited_perms]
338 320
339 321 RepoModel().delete(repo_name_full)
340 322 RepoGroupModel().delete(group_name)
341 323 Session().commit()
342 324
343 325 @pytest.mark.xfail_backends(
344 326 "git", "hg", reason="Missing reposerver support")
345 327 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
346 328 csrf_token):
347 329 source_repo = backend.create_repo(number_of_commits=2)
348 330 source_repo_name = source_repo.repo_name
349 331 reposerver.serve(source_repo.scm_instance())
350 332
351 333 repo_name = backend.new_repo_name()
352 334 response = self.app.post(
353 335 route_path('repo_create'),
354 336 fixture._get_repo_create_params(
355 337 repo_private=False,
356 338 repo_name=repo_name,
357 339 repo_type=backend.alias,
358 340 repo_description='',
359 341 clone_uri=reposerver.url,
360 342 csrf_token=csrf_token),
361 343 status=302)
362 344
363 345 # Should be redirected to the creating page
364 346 response.mustcontain('repo_creating')
365 347
366 348 # Expecting that both repositories have same history
367 349 source_repo = RepoModel().get_by_repo_name(source_repo_name)
368 350 source_vcs = source_repo.scm_instance()
369 351 repo = RepoModel().get_by_repo_name(repo_name)
370 352 repo_vcs = repo.scm_instance()
371 353 assert source_vcs[0].message == repo_vcs[0].message
372 354 assert source_vcs.count() == repo_vcs.count()
373 355 assert source_vcs.commit_ids == repo_vcs.commit_ids
374 356
375 357 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
376 358 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
377 359 csrf_token):
378 360 repo_name = backend.new_repo_name()
379 361 description = 'description for newly created repo'
380 362 response = self.app.post(
381 363 route_path('repo_create'),
382 364 fixture._get_repo_create_params(
383 365 repo_private=False,
384 366 repo_name=repo_name,
385 367 repo_type=backend.alias,
386 368 repo_description=description,
387 369 clone_uri='http://repo.invalid/repo',
388 370 csrf_token=csrf_token))
389 371 response.mustcontain('invalid clone url')
390 372
391 373 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
392 374 def test_create_remote_repo_wrong_clone_uri_hg_svn(
393 375 self, autologin_user, backend, csrf_token):
394 376 repo_name = backend.new_repo_name()
395 377 description = 'description for newly created repo'
396 378 response = self.app.post(
397 379 route_path('repo_create'),
398 380 fixture._get_repo_create_params(
399 381 repo_private=False,
400 382 repo_name=repo_name,
401 383 repo_type=backend.alias,
402 384 repo_description=description,
403 385 clone_uri='svn+http://svn.invalid/repo',
404 386 csrf_token=csrf_token))
405 387 response.mustcontain('invalid clone url')
406 388
407 389 def test_create_with_git_suffix(
408 390 self, autologin_user, backend, csrf_token):
409 391 repo_name = backend.new_repo_name() + ".git"
410 392 description = 'description for newly created repo'
411 393 response = self.app.post(
412 394 route_path('repo_create'),
413 395 fixture._get_repo_create_params(
414 396 repo_private=False,
415 397 repo_name=repo_name,
416 398 repo_type=backend.alias,
417 399 repo_description=description,
418 400 csrf_token=csrf_token))
419 401 response.mustcontain('Repository name cannot end with .git')
420 402
421 403 def test_default_user_cannot_access_private_repo_in_a_group(
422 404 self, autologin_user, user_util, backend):
423 405
424 406 group = user_util.create_repo_group()
425 407
426 408 repo = backend.create_repo(
427 409 repo_private=True, repo_group=group, repo_copy_permissions=True)
428 410
429 411 permissions = _get_permission_for_user(
430 412 user='default', repo=repo.repo_name)
431 413 assert len(permissions) == 1
432 414 assert permissions[0].permission.permission_name == 'repository.none'
433 415 assert permissions[0].repository.private is True
434 416
435 417 def test_create_on_top_level_without_permissions(self, backend):
436 418 session = login_user_session(
437 419 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
438 420 csrf_token = auth.get_csrf_token(session)
439 421
440 422 # revoke
441 423 user_model = UserModel()
442 424 # disable fork and create on default user
443 425 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
444 426 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
445 427 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
446 428 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
447 429
448 430 # disable on regular user
449 431 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
450 432 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
451 433 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
452 434 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
453 435 Session().commit()
454 436
455 437 repo_name = backend.new_repo_name()
456 438 description = 'description for newly created repo'
457 439 response = self.app.post(
458 440 route_path('repo_create'),
459 441 fixture._get_repo_create_params(
460 442 repo_private=False,
461 443 repo_name=repo_name,
462 444 repo_type=backend.alias,
463 445 repo_description=description,
464 446 csrf_token=csrf_token))
465 447
466 448 response.mustcontain(
467 449 u"You do not have the permission to store repositories in "
468 450 u"the root location.")
469 451
470 452 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
471 453 def test_create_repo_when_filesystem_op_fails(
472 454 self, autologin_user, backend, csrf_token):
473 455 repo_name = backend.new_repo_name()
474 456 description = 'description for newly created repo'
475 457
476 458 response = self.app.post(
477 459 route_path('repo_create'),
478 460 fixture._get_repo_create_params(
479 461 repo_private=False,
480 462 repo_name=repo_name,
481 463 repo_type=backend.alias,
482 464 repo_description=description,
483 465 csrf_token=csrf_token))
484 466
485 467 assert_session_flash(
486 468 response, 'Error creating repository %s' % repo_name)
487 469 # repo must not be in db
488 470 assert backend.repo is None
489 471 # repo must not be in filesystem !
490 472 assert not repo_on_filesystem(repo_name)
491 473
492 474 def assert_repository_is_created_correctly(self, repo_name, description, backend):
493 475 url_quoted_repo_name = urllib.parse.quote(repo_name)
494 476
495 477 # run the check page that triggers the flash message
496 478 response = self.app.get(
497 479 route_path('repo_creating_check', repo_name=repo_name))
498 480 assert response.json == {'result': True}
499 481
500 482 flash_msg = 'Created repository <a href="/{}">{}</a>'.format(url_quoted_repo_name, repo_name)
501 483 assert_session_flash(response, flash_msg)
502 484
503 485 # test if the repo was created in the database
504 486 new_repo = RepoModel().get_by_repo_name(repo_name)
505 487
506 488 assert new_repo.repo_name == repo_name
507 489 assert new_repo.description == description
508 490
509 491 # test if the repository is visible in the list ?
510 492 response = self.app.get(
511 493 h.route_path('repo_summary', repo_name=repo_name))
512 494 response.mustcontain(repo_name)
513 495 response.mustcontain(backend.alias)
514 496
515 497 assert repo_on_filesystem(repo_name)
@@ -1,195 +1,179 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21 import pytest
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.repo_group import RepoGroupModel
28 28 from rhodecode.tests import (
29 29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
30 30 from rhodecode.tests.fixture import Fixture
31
32 fixture = Fixture()
31 from rhodecode.tests.routes import route_path
33 32
34 33
35 def route_path(name, params=None, **kwargs):
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39
40 base_url = {
41 'repo_groups': ADMIN_PREFIX + '/repo_groups',
42 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
43 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
44 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
45
46 }[name].format(**kwargs)
47
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 return base_url
34 fixture = Fixture()
51 35
52 36
53 37 def _get_permission_for_user(user, repo):
54 38 perm = UserRepoToPerm.query()\
55 39 .filter(UserRepoToPerm.repository ==
56 40 Repository.get_by_repo_name(repo))\
57 41 .filter(UserRepoToPerm.user == User.get_by_username(user))\
58 42 .all()
59 43 return perm
60 44
61 45
62 46 @pytest.mark.usefixtures("app")
63 47 class TestAdminRepositoryGroups(object):
64 48
65 49 def test_show_repo_groups(self, autologin_user):
66 50 self.app.get(route_path('repo_groups'))
67 51
68 52 def test_show_repo_groups_data(self, autologin_user, xhr_header):
69 53 response = self.app.get(route_path(
70 54 'repo_groups_data'), extra_environ=xhr_header)
71 55
72 56 all_repo_groups = RepoGroup.query().count()
73 57 assert response.json['recordsTotal'] == all_repo_groups
74 58
75 59 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
76 60 response = self.app.get(route_path(
77 61 'repo_groups_data', params={'search[value]': 'empty_search'}),
78 62 extra_environ=xhr_header)
79 63
80 64 all_repo_groups = RepoGroup.query().count()
81 65 assert response.json['recordsTotal'] == all_repo_groups
82 66 assert response.json['recordsFiltered'] == 0
83 67
84 68 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
85 69 fixture.create_repo_group('test_repo_group')
86 70 response = self.app.get(route_path(
87 71 'repo_groups_data'), extra_environ=xhr_header)
88 72 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
89 73 fixture.destroy_repo_group('test_repo_group')
90 74
91 75 def test_new(self, autologin_user):
92 76 self.app.get(route_path('repo_group_new'))
93 77
94 78 def test_new_with_parent_group(self, autologin_user, user_util):
95 79 gr = user_util.create_repo_group()
96 80
97 81 self.app.get(route_path('repo_group_new'),
98 82 params=dict(parent_group=gr.group_name))
99 83
100 84 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
101 85 self.app.get(route_path('repo_group_new'), status=403)
102 86
103 87 @pytest.mark.parametrize('repo_group_name', [
104 88 'git_repo',
105 89 'git_repo_ąć',
106 90 'hg_repo',
107 91 '12345',
108 92 'hg_repo_ąć',
109 93 ])
110 94 def test_create(self, autologin_user, repo_group_name, csrf_token):
111 95 repo_group_name_non_ascii = repo_group_name
112 96 description = 'description for newly created repo group'
113 97
114 98 response = self.app.post(
115 99 route_path('repo_group_create'),
116 100 fixture._get_group_create_params(
117 101 group_name=repo_group_name,
118 102 group_description=description,
119 103 csrf_token=csrf_token))
120 104
121 105 # run the check page that triggers the flash message
122 106 repo_gr_url = h.route_path(
123 107 'repo_group_home', repo_group_name=repo_group_name)
124 108
125 109 assert_session_flash(
126 110 response,
127 111 'Created repository group <a href="%s">%s</a>' % (
128 112 repo_gr_url, repo_group_name_non_ascii))
129 113
130 114 # # test if the repo group was created in the database
131 115 new_repo_group = RepoGroupModel()._get_repo_group(
132 116 repo_group_name_non_ascii)
133 117 assert new_repo_group is not None
134 118
135 119 assert new_repo_group.group_name == repo_group_name_non_ascii
136 120 assert new_repo_group.group_description == description
137 121
138 122 # test if the repository is visible in the list ?
139 123 response = self.app.get(repo_gr_url)
140 124 response.mustcontain(repo_group_name)
141 125
142 126 # test if the repository group was created on filesystem
143 127 is_on_filesystem = os.path.isdir(
144 128 os.path.join(TESTS_TMP_PATH, repo_group_name))
145 129 if not is_on_filesystem:
146 130 self.fail('no repo group %s in filesystem' % repo_group_name)
147 131
148 132 RepoGroupModel().delete(repo_group_name_non_ascii)
149 133 Session().commit()
150 134
151 135 @pytest.mark.parametrize('repo_group_name', [
152 136 'git_repo',
153 137 'git_repo_ąć',
154 138 'hg_repo',
155 139 '12345',
156 140 'hg_repo_ąć',
157 141 ])
158 142 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
159 143 parent_group = user_util.create_repo_group()
160 144 parent_group_name = parent_group.group_name
161 145
162 146 expected_group_name = '{}/{}'.format(
163 147 parent_group_name, repo_group_name)
164 148 expected_group_name_non_ascii = expected_group_name
165 149
166 150 try:
167 151 response = self.app.post(
168 152 route_path('repo_group_create'),
169 153 fixture._get_group_create_params(
170 154 group_name=repo_group_name,
171 155 group_parent_id=parent_group.group_id,
172 156 group_description='Test desciption',
173 157 csrf_token=csrf_token))
174 158
175 159 assert_session_flash(
176 160 response,
177 161 u'Created repository group <a href="%s">%s</a>' % (
178 162 h.route_path('repo_group_home',
179 163 repo_group_name=expected_group_name),
180 164 expected_group_name_non_ascii))
181 165 finally:
182 166 RepoGroupModel().delete(expected_group_name_non_ascii)
183 167 Session().commit()
184 168
185 169 def test_user_with_creation_permissions_cannot_create_subgroups(
186 170 self, autologin_regular_user, user_util):
187 171
188 172 user_util.grant_user_permission(
189 173 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
190 174 parent_group = user_util.create_repo_group()
191 175 parent_group_id = parent_group.group_id
192 176 self.app.get(
193 177 route_path('repo_group_new',
194 178 params=dict(parent_group=parent_group_id), ),
195 179 status=403)
@@ -1,768 +1,695 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 25 from rhodecode.lib.hash_utils import md5_safe
26 26 from rhodecode.model.db import RhodeCodeUi
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
29 29 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests.routes import route_path
30 31
31 32
32 33 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
33 34
34 35
35 def route_path(name, params=None, **kwargs):
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
40
41 base_url = {
42
43 'admin_settings':
44 ADMIN_PREFIX +'/settings',
45 'admin_settings_update':
46 ADMIN_PREFIX + '/settings/update',
47 'admin_settings_global':
48 ADMIN_PREFIX + '/settings/global',
49 'admin_settings_global_update':
50 ADMIN_PREFIX + '/settings/global/update',
51 'admin_settings_vcs':
52 ADMIN_PREFIX + '/settings/vcs',
53 'admin_settings_vcs_update':
54 ADMIN_PREFIX + '/settings/vcs/update',
55 'admin_settings_vcs_svn_pattern_delete':
56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
57 'admin_settings_mapping':
58 ADMIN_PREFIX + '/settings/mapping',
59 'admin_settings_mapping_update':
60 ADMIN_PREFIX + '/settings/mapping/update',
61 'admin_settings_visual':
62 ADMIN_PREFIX + '/settings/visual',
63 'admin_settings_visual_update':
64 ADMIN_PREFIX + '/settings/visual/update',
65 'admin_settings_issuetracker':
66 ADMIN_PREFIX + '/settings/issue-tracker',
67 'admin_settings_issuetracker_update':
68 ADMIN_PREFIX + '/settings/issue-tracker/update',
69 'admin_settings_issuetracker_test':
70 ADMIN_PREFIX + '/settings/issue-tracker/test',
71 'admin_settings_issuetracker_delete':
72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
73 'admin_settings_email':
74 ADMIN_PREFIX + '/settings/email',
75 'admin_settings_email_update':
76 ADMIN_PREFIX + '/settings/email/update',
77 'admin_settings_hooks':
78 ADMIN_PREFIX + '/settings/hooks',
79 'admin_settings_hooks_update':
80 ADMIN_PREFIX + '/settings/hooks/update',
81 'admin_settings_hooks_delete':
82 ADMIN_PREFIX + '/settings/hooks/delete',
83 'admin_settings_search':
84 ADMIN_PREFIX + '/settings/search',
85 'admin_settings_labs':
86 ADMIN_PREFIX + '/settings/labs',
87 'admin_settings_labs_update':
88 ADMIN_PREFIX + '/settings/labs/update',
89
90 'admin_settings_sessions':
91 ADMIN_PREFIX + '/settings/sessions',
92 'admin_settings_sessions_cleanup':
93 ADMIN_PREFIX + '/settings/sessions/cleanup',
94 'admin_settings_system':
95 ADMIN_PREFIX + '/settings/system',
96 'admin_settings_system_update':
97 ADMIN_PREFIX + '/settings/system/updates',
98 'admin_settings_open_source':
99 ADMIN_PREFIX + '/settings/open_source',
100
101
102 }[name].format(**kwargs)
103
104 if params:
105 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
106 return base_url
107
108
109 36 @pytest.mark.usefixtures('autologin_user', 'app')
110 37 class TestAdminSettingsController(object):
111 38
112 39 @pytest.mark.parametrize('urlname', [
113 40 'admin_settings_vcs',
114 41 'admin_settings_mapping',
115 42 'admin_settings_global',
116 43 'admin_settings_visual',
117 44 'admin_settings_email',
118 45 'admin_settings_hooks',
119 46 'admin_settings_search',
120 47 ])
121 48 def test_simple_get(self, urlname):
122 49 self.app.get(route_path(urlname))
123 50
124 51 def test_create_custom_hook(self, csrf_token):
125 52 response = self.app.post(
126 53 route_path('admin_settings_hooks_update'),
127 54 params={
128 55 'new_hook_ui_key': 'test_hooks_1',
129 56 'new_hook_ui_value': 'cd /tmp',
130 57 'csrf_token': csrf_token})
131 58
132 59 response = response.follow()
133 60 response.mustcontain('test_hooks_1')
134 61 response.mustcontain('cd /tmp')
135 62
136 63 def test_create_custom_hook_delete(self, csrf_token):
137 64 response = self.app.post(
138 65 route_path('admin_settings_hooks_update'),
139 66 params={
140 67 'new_hook_ui_key': 'test_hooks_2',
141 68 'new_hook_ui_value': 'cd /tmp2',
142 69 'csrf_token': csrf_token})
143 70
144 71 response = response.follow()
145 72 response.mustcontain('test_hooks_2')
146 73 response.mustcontain('cd /tmp2')
147 74
148 75 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149 76
150 77 # delete
151 78 self.app.post(
152 79 route_path('admin_settings_hooks_delete'),
153 80 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 81 response = self.app.get(route_path('admin_settings_hooks'))
155 82 response.mustcontain(no=['test_hooks_2'])
156 83 response.mustcontain(no=['cd /tmp2'])
157 84
158 85
159 86 @pytest.mark.usefixtures('autologin_user', 'app')
160 87 class TestAdminSettingsGlobal(object):
161 88
162 89 def test_pre_post_code_code_active(self, csrf_token):
163 90 pre_code = 'rc-pre-code-187652122'
164 91 post_code = 'rc-postcode-98165231'
165 92
166 93 response = self.post_and_verify_settings({
167 94 'rhodecode_pre_code': pre_code,
168 95 'rhodecode_post_code': post_code,
169 96 'csrf_token': csrf_token,
170 97 })
171 98
172 99 response = response.follow()
173 100 response.mustcontain(pre_code, post_code)
174 101
175 102 def test_pre_post_code_code_inactive(self, csrf_token):
176 103 pre_code = 'rc-pre-code-187652122'
177 104 post_code = 'rc-postcode-98165231'
178 105 response = self.post_and_verify_settings({
179 106 'rhodecode_pre_code': '',
180 107 'rhodecode_post_code': '',
181 108 'csrf_token': csrf_token,
182 109 })
183 110
184 111 response = response.follow()
185 112 response.mustcontain(no=[pre_code, post_code])
186 113
187 114 def test_captcha_activate(self, csrf_token):
188 115 self.post_and_verify_settings({
189 116 'rhodecode_captcha_private_key': '1234567890',
190 117 'rhodecode_captcha_public_key': '1234567890',
191 118 'csrf_token': csrf_token,
192 119 })
193 120
194 121 response = self.app.get(ADMIN_PREFIX + '/register')
195 122 response.mustcontain('captcha')
196 123
197 124 def test_captcha_deactivate(self, csrf_token):
198 125 self.post_and_verify_settings({
199 126 'rhodecode_captcha_private_key': '',
200 127 'rhodecode_captcha_public_key': '1234567890',
201 128 'csrf_token': csrf_token,
202 129 })
203 130
204 131 response = self.app.get(ADMIN_PREFIX + '/register')
205 132 response.mustcontain(no=['captcha'])
206 133
207 134 def test_title_change(self, csrf_token):
208 135 old_title = 'RhodeCode'
209 136
210 137 for new_title in ['Changed', 'Żółwik', old_title]:
211 138 response = self.post_and_verify_settings({
212 139 'rhodecode_title': new_title,
213 140 'csrf_token': csrf_token,
214 141 })
215 142
216 143 response = response.follow()
217 144 response.mustcontain(new_title)
218 145
219 146 def post_and_verify_settings(self, settings):
220 147 old_title = 'RhodeCode'
221 148 old_realm = 'RhodeCode authentication'
222 149 params = {
223 150 'rhodecode_title': old_title,
224 151 'rhodecode_realm': old_realm,
225 152 'rhodecode_pre_code': '',
226 153 'rhodecode_post_code': '',
227 154 'rhodecode_captcha_private_key': '',
228 155 'rhodecode_captcha_public_key': '',
229 156 'rhodecode_create_personal_repo_group': False,
230 157 'rhodecode_personal_repo_group_pattern': '${username}',
231 158 }
232 159 params.update(settings)
233 160 response = self.app.post(
234 161 route_path('admin_settings_global_update'), params=params)
235 162
236 163 assert_session_flash(response, 'Updated application settings')
237 164
238 165 app_settings = SettingsModel().get_all_settings()
239 166 del settings['csrf_token']
240 167 for key, value in settings.items():
241 168 assert app_settings[key] == value
242 169
243 170 return response
244 171
245 172
246 173 @pytest.mark.usefixtures('autologin_user', 'app')
247 174 class TestAdminSettingsVcs(object):
248 175
249 176 def test_contains_svn_default_patterns(self):
250 177 response = self.app.get(route_path('admin_settings_vcs'))
251 178 expected_patterns = [
252 179 '/trunk',
253 180 '/branches/*',
254 181 '/tags/*',
255 182 ]
256 183 for pattern in expected_patterns:
257 184 response.mustcontain(pattern)
258 185
259 186 def test_add_new_svn_branch_and_tag_pattern(
260 187 self, backend_svn, form_defaults, disable_sql_cache,
261 188 csrf_token):
262 189 form_defaults.update({
263 190 'new_svn_branch': '/exp/branches/*',
264 191 'new_svn_tag': '/important_tags/*',
265 192 'csrf_token': csrf_token,
266 193 })
267 194
268 195 response = self.app.post(
269 196 route_path('admin_settings_vcs_update'),
270 197 params=form_defaults, status=302)
271 198 response = response.follow()
272 199
273 200 # Expect to find the new values on the page
274 201 response.mustcontain('/exp/branches/*')
275 202 response.mustcontain('/important_tags/*')
276 203
277 204 # Expect that those patterns are used to match branches and tags now
278 205 repo = backend_svn['svn-simple-layout'].scm_instance()
279 206 assert 'exp/branches/exp-sphinx-docs' in repo.branches
280 207 assert 'important_tags/v0.5' in repo.tags
281 208
282 209 def test_add_same_svn_value_twice_shows_an_error_message(
283 210 self, form_defaults, csrf_token, settings_util):
284 211 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
285 212 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
286 213
287 214 response = self.app.post(
288 215 route_path('admin_settings_vcs_update'),
289 216 params={
290 217 'paths_root_path': form_defaults['paths_root_path'],
291 218 'new_svn_branch': '/test',
292 219 'new_svn_tag': '/test',
293 220 'csrf_token': csrf_token,
294 221 },
295 222 status=200)
296 223
297 224 response.mustcontain("Pattern already exists")
298 225 response.mustcontain("Some form inputs contain invalid data.")
299 226
300 227 @pytest.mark.parametrize('section', [
301 228 'vcs_svn_branch',
302 229 'vcs_svn_tag',
303 230 ])
304 231 def test_delete_svn_patterns(
305 232 self, section, csrf_token, settings_util):
306 233 setting = settings_util.create_rhodecode_ui(
307 234 section, '/test_delete', cleanup=False)
308 235
309 236 self.app.post(
310 237 route_path('admin_settings_vcs_svn_pattern_delete'),
311 238 params={
312 239 'delete_svn_pattern': setting.ui_id,
313 240 'csrf_token': csrf_token},
314 241 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
315 242
316 243 @pytest.mark.parametrize('section', [
317 244 'vcs_svn_branch',
318 245 'vcs_svn_tag',
319 246 ])
320 247 def test_delete_svn_patterns_raises_404_when_no_xhr(
321 248 self, section, csrf_token, settings_util):
322 249 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
323 250
324 251 self.app.post(
325 252 route_path('admin_settings_vcs_svn_pattern_delete'),
326 253 params={
327 254 'delete_svn_pattern': setting.ui_id,
328 255 'csrf_token': csrf_token},
329 256 status=404)
330 257
331 258 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
332 259 form_defaults.update({
333 260 'csrf_token': csrf_token,
334 261 'extensions_hgsubversion': 'True',
335 262 })
336 263 response = self.app.post(
337 264 route_path('admin_settings_vcs_update'),
338 265 params=form_defaults,
339 266 status=302)
340 267
341 268 response = response.follow()
342 269 extensions_input = (
343 270 '<input id="extensions_hgsubversion" '
344 271 'name="extensions_hgsubversion" type="checkbox" '
345 272 'value="True" checked="checked" />')
346 273 response.mustcontain(extensions_input)
347 274
348 275 def test_extensions_hgevolve(self, form_defaults, csrf_token):
349 276 form_defaults.update({
350 277 'csrf_token': csrf_token,
351 278 'extensions_evolve': 'True',
352 279 })
353 280 response = self.app.post(
354 281 route_path('admin_settings_vcs_update'),
355 282 params=form_defaults,
356 283 status=302)
357 284
358 285 response = response.follow()
359 286 extensions_input = (
360 287 '<input id="extensions_evolve" '
361 288 'name="extensions_evolve" type="checkbox" '
362 289 'value="True" checked="checked" />')
363 290 response.mustcontain(extensions_input)
364 291
365 292 def test_has_a_section_for_pull_request_settings(self):
366 293 response = self.app.get(route_path('admin_settings_vcs'))
367 294 response.mustcontain('Pull Request Settings')
368 295
369 296 def test_has_an_input_for_invalidation_of_inline_comments(self):
370 297 response = self.app.get(route_path('admin_settings_vcs'))
371 298 assert_response = response.assert_response()
372 299 assert_response.one_element_exists(
373 300 '[name=rhodecode_use_outdated_comments]')
374 301
375 302 @pytest.mark.parametrize('new_value', [True, False])
376 303 def test_allows_to_change_invalidation_of_inline_comments(
377 304 self, form_defaults, csrf_token, new_value):
378 305 setting_key = 'use_outdated_comments'
379 306 setting = SettingsModel().create_or_update_setting(
380 307 setting_key, not new_value, 'bool')
381 308 Session().add(setting)
382 309 Session().commit()
383 310
384 311 form_defaults.update({
385 312 'csrf_token': csrf_token,
386 313 'rhodecode_use_outdated_comments': str(new_value),
387 314 })
388 315 response = self.app.post(
389 316 route_path('admin_settings_vcs_update'),
390 317 params=form_defaults,
391 318 status=302)
392 319 response = response.follow()
393 320 setting = SettingsModel().get_setting_by_name(setting_key)
394 321 assert setting.app_settings_value is new_value
395 322
396 323 @pytest.mark.parametrize('new_value', [True, False])
397 324 def test_allows_to_change_hg_rebase_merge_strategy(
398 325 self, form_defaults, csrf_token, new_value):
399 326 setting_key = 'hg_use_rebase_for_merging'
400 327
401 328 form_defaults.update({
402 329 'csrf_token': csrf_token,
403 330 'rhodecode_' + setting_key: str(new_value),
404 331 })
405 332
406 333 with mock.patch.dict(
407 334 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
408 335 self.app.post(
409 336 route_path('admin_settings_vcs_update'),
410 337 params=form_defaults,
411 338 status=302)
412 339
413 340 setting = SettingsModel().get_setting_by_name(setting_key)
414 341 assert setting.app_settings_value is new_value
415 342
416 343 @pytest.fixture()
417 344 def disable_sql_cache(self, request):
418 345 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
419 346 patcher = mock.patch(
420 347 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
421 348 request.addfinalizer(patcher.stop)
422 349 patcher.start()
423 350
424 351 @pytest.fixture()
425 352 def form_defaults(self):
426 353 from rhodecode.apps.admin.views.settings import AdminSettingsView
427 354 return AdminSettingsView._form_defaults()
428 355
429 356 # TODO: johbo: What we really want is to checkpoint before a test run and
430 357 # reset the session afterwards.
431 358 @pytest.fixture(scope='class', autouse=True)
432 359 def cleanup_settings(self, request, baseapp):
433 360 ui_id = RhodeCodeUi.ui_id
434 361 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
435 362
436 363 @request.addfinalizer
437 364 def cleanup():
438 365 RhodeCodeUi.query().filter(
439 366 ui_id.notin_(original_ids)).delete(False)
440 367
441 368
442 369 @pytest.mark.usefixtures('autologin_user', 'app')
443 370 class TestLabsSettings(object):
444 371 def test_get_settings_page_disabled(self):
445 372 with mock.patch.dict(
446 373 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
447 374
448 375 response = self.app.get(
449 376 route_path('admin_settings_labs'), status=302)
450 377
451 378 assert response.location.endswith(route_path('admin_settings'))
452 379
453 380 def test_get_settings_page_enabled(self):
454 381 from rhodecode.apps.admin.views import settings
455 382 lab_settings = [
456 383 settings.LabSetting(
457 384 key='rhodecode_bool',
458 385 type='bool',
459 386 group='bool group',
460 387 label='bool label',
461 388 help='bool help'
462 389 ),
463 390 settings.LabSetting(
464 391 key='rhodecode_text',
465 392 type='unicode',
466 393 group='text group',
467 394 label='text label',
468 395 help='text help'
469 396 ),
470 397 ]
471 398 with mock.patch.dict(rhodecode.CONFIG,
472 399 {'labs_settings_active': 'true'}):
473 400 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
474 401 response = self.app.get(route_path('admin_settings_labs'))
475 402
476 403 assert '<label>bool group:</label>' in response
477 404 assert '<label for="rhodecode_bool">bool label</label>' in response
478 405 assert '<p class="help-block">bool help</p>' in response
479 406 assert 'name="rhodecode_bool" type="checkbox"' in response
480 407
481 408 assert '<label>text group:</label>' in response
482 409 assert '<label for="rhodecode_text">text label</label>' in response
483 410 assert '<p class="help-block">text help</p>' in response
484 411 assert 'name="rhodecode_text" size="60" type="text"' in response
485 412
486 413
487 414 @pytest.mark.usefixtures('app')
488 415 class TestOpenSourceLicenses(object):
489 416
490 417 def test_records_are_displayed(self, autologin_user):
491 418 sample_licenses = [
492 419 {
493 420 "license": [
494 421 {
495 422 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
496 423 "shortName": "bsdOriginal",
497 424 "spdxId": "BSD-4-Clause",
498 425 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
499 426 }
500 427 ],
501 428 "name": "python2.7-coverage-3.7.1"
502 429 },
503 430 {
504 431 "license": [
505 432 {
506 433 "fullName": "MIT License",
507 434 "shortName": "mit",
508 435 "spdxId": "MIT",
509 436 "url": "http://spdx.org/licenses/MIT.html"
510 437 }
511 438 ],
512 439 "name": "python2.7-bootstrapped-pip-9.0.1"
513 440 },
514 441 ]
515 442 read_licenses_patch = mock.patch(
516 443 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
517 444 return_value=sample_licenses)
518 445 with read_licenses_patch:
519 446 response = self.app.get(
520 447 route_path('admin_settings_open_source'), status=200)
521 448
522 449 assert_response = response.assert_response()
523 450 assert_response.element_contains(
524 451 '.panel-heading', 'Licenses of Third Party Packages')
525 452 for license_data in sample_licenses:
526 453 response.mustcontain(license_data["license"][0]["spdxId"])
527 454 assert_response.element_contains('.panel-body', license_data["name"])
528 455
529 456 def test_records_can_be_read(self, autologin_user):
530 457 response = self.app.get(
531 458 route_path('admin_settings_open_source'), status=200)
532 459 assert_response = response.assert_response()
533 460 assert_response.element_contains(
534 461 '.panel-heading', 'Licenses of Third Party Packages')
535 462
536 463 def test_forbidden_when_normal_user(self, autologin_regular_user):
537 464 self.app.get(
538 465 route_path('admin_settings_open_source'), status=404)
539 466
540 467
541 468 @pytest.mark.usefixtures('app')
542 469 class TestUserSessions(object):
543 470
544 471 def test_forbidden_when_normal_user(self, autologin_regular_user):
545 472 self.app.get(route_path('admin_settings_sessions'), status=404)
546 473
547 474 def test_show_sessions_page(self, autologin_user):
548 475 response = self.app.get(route_path('admin_settings_sessions'), status=200)
549 476 response.mustcontain('file')
550 477
551 478 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
552 479
553 480 post_data = {
554 481 'csrf_token': csrf_token,
555 482 'expire_days': '60'
556 483 }
557 484 response = self.app.post(
558 485 route_path('admin_settings_sessions_cleanup'), params=post_data,
559 486 status=302)
560 487 assert_session_flash(response, 'Cleaned up old sessions')
561 488
562 489
563 490 @pytest.mark.usefixtures('app')
564 491 class TestAdminSystemInfo(object):
565 492
566 493 def test_forbidden_when_normal_user(self, autologin_regular_user):
567 494 self.app.get(route_path('admin_settings_system'), status=404)
568 495
569 496 def test_system_info_page(self, autologin_user):
570 497 response = self.app.get(route_path('admin_settings_system'))
571 498 response.mustcontain('RhodeCode Community Edition, version {}'.format(
572 499 rhodecode.__version__))
573 500
574 501 def test_system_update_new_version(self, autologin_user):
575 502 update_data = {
576 503 'versions': [
577 504 {
578 505 'version': '100.3.1415926535',
579 506 'general': 'The latest version we are ever going to ship'
580 507 },
581 508 {
582 509 'version': '0.0.0',
583 510 'general': 'The first version we ever shipped'
584 511 }
585 512 ]
586 513 }
587 514 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
588 515 response = self.app.get(route_path('admin_settings_system_update'))
589 516 response.mustcontain('A <b>new version</b> is available')
590 517
591 518 def test_system_update_nothing_new(self, autologin_user):
592 519 update_data = {
593 520 'versions': [
594 521 {
595 522 'version': '0.0.0',
596 523 'general': 'The first version we ever shipped'
597 524 }
598 525 ]
599 526 }
600 527 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
601 528 response = self.app.get(route_path('admin_settings_system_update'))
602 529 response.mustcontain(
603 530 'This instance is already running the <b>latest</b> stable version')
604 531
605 532 def test_system_update_bad_response(self, autologin_user):
606 533 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
607 534 response = self.app.get(route_path('admin_settings_system_update'))
608 535 response.mustcontain(
609 536 'Bad data sent from update server')
610 537
611 538
612 539 @pytest.mark.usefixtures("app")
613 540 class TestAdminSettingsIssueTracker(object):
614 541 RC_PREFIX = 'rhodecode_'
615 542 SHORT_PATTERN_KEY = 'issuetracker_pat_'
616 543 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
617 544 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
618 545
619 546 def test_issuetracker_index(self, autologin_user):
620 547 response = self.app.get(route_path('admin_settings_issuetracker'))
621 548 assert response.status_code == 200
622 549
623 550 def test_add_empty_issuetracker_pattern(
624 551 self, request, autologin_user, csrf_token):
625 552 post_url = route_path('admin_settings_issuetracker_update')
626 553 post_data = {
627 554 'csrf_token': csrf_token
628 555 }
629 556 self.app.post(post_url, post_data, status=302)
630 557
631 558 def test_add_issuetracker_pattern(
632 559 self, request, autologin_user, csrf_token):
633 560 pattern = 'issuetracker_pat'
634 561 another_pattern = pattern+'1'
635 562 post_url = route_path('admin_settings_issuetracker_update')
636 563 post_data = {
637 564 'new_pattern_pattern_0': pattern,
638 565 'new_pattern_url_0': 'http://url',
639 566 'new_pattern_prefix_0': 'prefix',
640 567 'new_pattern_description_0': 'description',
641 568 'new_pattern_pattern_1': another_pattern,
642 569 'new_pattern_url_1': 'https://url1',
643 570 'new_pattern_prefix_1': 'prefix1',
644 571 'new_pattern_description_1': 'description1',
645 572 'csrf_token': csrf_token
646 573 }
647 574 self.app.post(post_url, post_data, status=302)
648 575 settings = SettingsModel().get_all_settings()
649 576 self.uid = md5_safe(pattern)
650 577 assert settings[self.PATTERN_KEY+self.uid] == pattern
651 578 self.another_uid = md5_safe(another_pattern)
652 579 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
653 580
654 581 @request.addfinalizer
655 582 def cleanup():
656 583 defaults = SettingsModel().get_all_settings()
657 584
658 585 entries = [name for name in defaults if (
659 586 (self.uid in name) or (self.another_uid in name))]
660 587 start = len(self.RC_PREFIX)
661 588 for del_key in entries:
662 589 # TODO: anderson: get_by_name needs name without prefix
663 590 entry = SettingsModel().get_setting_by_name(del_key[start:])
664 591 Session().delete(entry)
665 592
666 593 Session().commit()
667 594
668 595 def test_edit_issuetracker_pattern(
669 596 self, autologin_user, backend, csrf_token, request):
670 597
671 598 old_pattern = 'issuetracker_pat1'
672 599 old_uid = md5_safe(old_pattern)
673 600
674 601 post_url = route_path('admin_settings_issuetracker_update')
675 602 post_data = {
676 603 'new_pattern_pattern_0': old_pattern,
677 604 'new_pattern_url_0': 'http://url',
678 605 'new_pattern_prefix_0': 'prefix',
679 606 'new_pattern_description_0': 'description',
680 607
681 608 'csrf_token': csrf_token
682 609 }
683 610 self.app.post(post_url, post_data, status=302)
684 611
685 612 new_pattern = 'issuetracker_pat1_edited'
686 613 self.new_uid = md5_safe(new_pattern)
687 614
688 615 post_url = route_path('admin_settings_issuetracker_update')
689 616 post_data = {
690 617 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
691 618 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
692 619 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
693 620 'new_pattern_description_{}'.format(old_uid): 'description_edited',
694 621 'uid': old_uid,
695 622 'csrf_token': csrf_token
696 623 }
697 624 self.app.post(post_url, post_data, status=302)
698 625
699 626 settings = SettingsModel().get_all_settings()
700 627 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
701 628 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
702 629 assert self.PATTERN_KEY+old_uid not in settings
703 630
704 631 @request.addfinalizer
705 632 def cleanup():
706 633 IssueTrackerSettingsModel().delete_entries(old_uid)
707 634 IssueTrackerSettingsModel().delete_entries(self.new_uid)
708 635
709 636 def test_replace_issuetracker_pattern_description(
710 637 self, autologin_user, csrf_token, request, settings_util):
711 638 prefix = 'issuetracker'
712 639 pattern = 'issuetracker_pat'
713 640 self.uid = md5_safe(pattern)
714 641 pattern_key = '_'.join([prefix, 'pat', self.uid])
715 642 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
716 643 desc_key = '_'.join([prefix, 'desc', self.uid])
717 644 rc_desc_key = '_'.join(['rhodecode', desc_key])
718 645 new_description = 'new_description'
719 646
720 647 settings_util.create_rhodecode_setting(
721 648 pattern_key, pattern, 'unicode', cleanup=False)
722 649 settings_util.create_rhodecode_setting(
723 650 desc_key, 'old description', 'unicode', cleanup=False)
724 651
725 652 post_url = route_path('admin_settings_issuetracker_update')
726 653 post_data = {
727 654 'new_pattern_pattern_0': pattern,
728 655 'new_pattern_url_0': 'https://url',
729 656 'new_pattern_prefix_0': 'prefix',
730 657 'new_pattern_description_0': new_description,
731 658 'uid': self.uid,
732 659 'csrf_token': csrf_token
733 660 }
734 661 self.app.post(post_url, post_data, status=302)
735 662 settings = SettingsModel().get_all_settings()
736 663 assert settings[rc_pattern_key] == pattern
737 664 assert settings[rc_desc_key] == new_description
738 665
739 666 @request.addfinalizer
740 667 def cleanup():
741 668 IssueTrackerSettingsModel().delete_entries(self.uid)
742 669
743 670 def test_delete_issuetracker_pattern(
744 671 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
745 672
746 673 old_pattern = 'issuetracker_pat_deleted'
747 674 old_uid = md5_safe(old_pattern)
748 675
749 676 post_url = route_path('admin_settings_issuetracker_update')
750 677 post_data = {
751 678 'new_pattern_pattern_0': old_pattern,
752 679 'new_pattern_url_0': 'http://url',
753 680 'new_pattern_prefix_0': 'prefix',
754 681 'new_pattern_description_0': 'description',
755 682
756 683 'csrf_token': csrf_token
757 684 }
758 685 self.app.post(post_url, post_data, status=302)
759 686
760 687 post_url = route_path('admin_settings_issuetracker_delete')
761 688 post_data = {
762 689 'uid': old_uid,
763 690 'csrf_token': csrf_token
764 691 }
765 692 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
766 693 settings = SettingsModel().get_all_settings()
767 694 assert self.PATTERN_KEY+old_uid not in settings
768 695 assert self.DESC_KEY + old_uid not in settings
@@ -1,171 +1,152 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.model.db import UserGroup, User
23 23 from rhodecode.model.meta import Session
24 24
25 25 from rhodecode.tests import (
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
26 TestController, assert_session_flash)
27 27 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.routes import route_path
28 29
29 30 fixture = Fixture()
30 31
31 32
32 def route_path(name, params=None, **kwargs):
33 import urllib.request
34 import urllib.parse
35 import urllib.error
36 from rhodecode.apps._base import ADMIN_PREFIX
37
38 base_url = {
39 'user_groups': ADMIN_PREFIX + '/user_groups',
40 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
41 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
42 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
43 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
44 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
45 }[name].format(**kwargs)
46
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
50
51
52 33 class TestAdminUserGroupsView(TestController):
53 34
54 35 def test_show_users(self):
55 36 self.log_user()
56 37 self.app.get(route_path('user_groups'))
57 38
58 39 def test_show_user_groups_data(self, xhr_header):
59 40 self.log_user()
60 41 response = self.app.get(route_path(
61 42 'user_groups_data'), extra_environ=xhr_header)
62 43
63 44 all_user_groups = UserGroup.query().count()
64 45 assert response.json['recordsTotal'] == all_user_groups
65 46
66 47 def test_show_user_groups_data_filtered(self, xhr_header):
67 48 self.log_user()
68 49 response = self.app.get(route_path(
69 50 'user_groups_data', params={'search[value]': 'empty_search'}),
70 51 extra_environ=xhr_header)
71 52
72 53 all_user_groups = UserGroup.query().count()
73 54 assert response.json['recordsTotal'] == all_user_groups
74 55 assert response.json['recordsFiltered'] == 0
75 56
76 57 def test_usergroup_escape(self, user_util, xhr_header):
77 58 self.log_user()
78 59
79 60 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
80 61 user = user_util.create_user()
81 62 user.name = xss_img
82 63 user.lastname = xss_img
83 64 Session().add(user)
84 65 Session().commit()
85 66
86 67 user_group = user_util.create_user_group()
87 68
88 69 user_group.users_group_name = xss_img
89 70 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
90 71
91 72 response = self.app.get(
92 73 route_path('user_groups_data'), extra_environ=xhr_header)
93 74
94 75 response.mustcontain(
95 76 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
96 77 response.mustcontain(
97 78 '&lt;img src=&#34;/image1&#34; onload=&#34;'
98 79 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
99 80
100 81 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
101 82 self.log_user()
102 83 ug = user_util.create_user_group()
103 84 response = self.app.get(
104 85 route_path('user_group_members_data', user_group_id=ug.users_group_id),
105 86 extra_environ=xhr_header)
106 87
107 88 assert response.json == {'members': []}
108 89
109 90 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
110 91 self.log_user()
111 92 members = [u.user_id for u in User.get_all()]
112 93 ug = user_util.create_user_group(members=members)
113 94 response = self.app.get(
114 95 route_path('user_group_members_data',
115 96 user_group_id=ug.users_group_id),
116 97 extra_environ=xhr_header)
117 98
118 99 assert len(response.json['members']) == len(members)
119 100
120 101 def test_creation_page(self):
121 102 self.log_user()
122 103 self.app.get(route_path('user_groups_new'), status=200)
123 104
124 105 def test_create(self):
125 106 from rhodecode.lib import helpers as h
126 107
127 108 self.log_user()
128 109 users_group_name = 'test_user_group'
129 110 response = self.app.post(route_path('user_groups_create'), {
130 111 'users_group_name': users_group_name,
131 112 'user_group_description': 'DESC',
132 113 'active': True,
133 114 'csrf_token': self.csrf_token})
134 115
135 116 user_group_id = UserGroup.get_by_group_name(
136 117 users_group_name).users_group_id
137 118
138 119 user_group_link = h.link_to(
139 120 users_group_name,
140 121 route_path('edit_user_group', user_group_id=user_group_id))
141 122
142 123 assert_session_flash(
143 124 response,
144 125 'Created user group %s' % user_group_link)
145 126
146 127 fixture.destroy_user_group(users_group_name)
147 128
148 129 def test_create_with_empty_name(self):
149 130 self.log_user()
150 131
151 132 response = self.app.post(route_path('user_groups_create'), {
152 133 'users_group_name': '',
153 134 'user_group_description': 'DESC',
154 135 'active': True,
155 136 'csrf_token': self.csrf_token}, status=200)
156 137
157 138 response.mustcontain('Please enter a value')
158 139
159 140 def test_create_duplicate(self, user_util):
160 141 self.log_user()
161 142
162 143 user_group = user_util.create_user_group()
163 144 duplicate_name = user_group.users_group_name
164 145 response = self.app.post(route_path('user_groups_create'), {
165 146 'users_group_name': duplicate_name,
166 147 'user_group_description': 'DESC',
167 148 'active': True,
168 149 'csrf_token': self.csrf_token}, status=200)
169 150
170 151 response.mustcontain(
171 152 'User group `{}` already exists'.format(duplicate_name))
@@ -1,795 +1,727 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21 from sqlalchemy.orm.exc import NoResultFound
22 22
23 23 from rhodecode.lib import auth
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.user import UserModel
28 28
29 29 from rhodecode.tests import (
30 30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
31 31 from rhodecode.tests.fixture import Fixture
32 from rhodecode.tests.routes import route_path
32 33
33 34 fixture = Fixture()
34 35
35 36
36 def route_path(name, params=None, **kwargs):
37 import urllib.request
38 import urllib.parse
39 import urllib.error
40 from rhodecode.apps._base import ADMIN_PREFIX
41
42 base_url = {
43 'users':
44 ADMIN_PREFIX + '/users',
45 'users_data':
46 ADMIN_PREFIX + '/users_data',
47 'users_create':
48 ADMIN_PREFIX + '/users/create',
49 'users_new':
50 ADMIN_PREFIX + '/users/new',
51 'user_edit':
52 ADMIN_PREFIX + '/users/{user_id}/edit',
53 'user_edit_advanced':
54 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
55 'user_edit_global_perms':
56 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
57 'user_edit_global_perms_update':
58 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
59 'user_update':
60 ADMIN_PREFIX + '/users/{user_id}/update',
61 'user_delete':
62 ADMIN_PREFIX + '/users/{user_id}/delete',
63 'user_create_personal_repo_group':
64 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
65
66 'edit_user_auth_tokens':
67 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
68 'edit_user_auth_tokens_add':
69 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
70 'edit_user_auth_tokens_delete':
71 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
72
73 'edit_user_emails':
74 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
75 'edit_user_emails_add':
76 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
77 'edit_user_emails_delete':
78 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
79
80 'edit_user_ips':
81 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
82 'edit_user_ips_add':
83 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
84 'edit_user_ips_delete':
85 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
86
87 'edit_user_perms_summary':
88 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
89 'edit_user_perms_summary_json':
90 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
91
92 'edit_user_audit_logs':
93 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
94
95 'edit_user_audit_logs_download':
96 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
97
98 }[name].format(**kwargs)
99
100 if params:
101 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
102 return base_url
103
104
105 37 class TestAdminUsersView(TestController):
106 38
107 39 def test_show_users(self):
108 40 self.log_user()
109 41 self.app.get(route_path('users'))
110 42
111 43 def test_show_users_data(self, xhr_header):
112 44 self.log_user()
113 45 response = self.app.get(route_path(
114 46 'users_data'), extra_environ=xhr_header)
115 47
116 48 all_users = User.query().filter(
117 49 User.username != User.DEFAULT_USER).count()
118 50 assert response.json['recordsTotal'] == all_users
119 51
120 52 def test_show_users_data_filtered(self, xhr_header):
121 53 self.log_user()
122 54 response = self.app.get(route_path(
123 55 'users_data', params={'search[value]': 'empty_search'}),
124 56 extra_environ=xhr_header)
125 57
126 58 all_users = User.query().filter(
127 59 User.username != User.DEFAULT_USER).count()
128 60 assert response.json['recordsTotal'] == all_users
129 61 assert response.json['recordsFiltered'] == 0
130 62
131 63 def test_auth_tokens_default_user(self):
132 64 self.log_user()
133 65 user = User.get_default_user()
134 66 response = self.app.get(
135 67 route_path('edit_user_auth_tokens', user_id=user.user_id),
136 68 status=302)
137 69
138 70 def test_auth_tokens(self):
139 71 self.log_user()
140 72
141 73 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
142 74 user_id = user.user_id
143 75 auth_tokens = user.auth_tokens
144 76 response = self.app.get(
145 77 route_path('edit_user_auth_tokens', user_id=user_id))
146 78 for token in auth_tokens:
147 79 response.mustcontain(token[:4])
148 80 response.mustcontain('never')
149 81
150 82 @pytest.mark.parametrize("desc, lifetime", [
151 83 ('forever', -1),
152 84 ('5mins', 60*5),
153 85 ('30days', 60*60*24*30),
154 86 ])
155 87 def test_add_auth_token(self, desc, lifetime, user_util):
156 88 self.log_user()
157 89 user = user_util.create_user()
158 90 user_id = user.user_id
159 91
160 92 response = self.app.post(
161 93 route_path('edit_user_auth_tokens_add', user_id=user_id),
162 94 {'description': desc, 'lifetime': lifetime,
163 95 'csrf_token': self.csrf_token})
164 96 assert_session_flash(response, 'Auth token successfully created')
165 97
166 98 response = response.follow()
167 99 user = User.get(user_id)
168 100 for auth_token in user.auth_tokens:
169 101 response.mustcontain(auth_token[:4])
170 102
171 103 def test_delete_auth_token(self, user_util):
172 104 self.log_user()
173 105 user = user_util.create_user()
174 106 user_id = user.user_id
175 107 keys = user.auth_tokens
176 108 assert 2 == len(keys)
177 109
178 110 response = self.app.post(
179 111 route_path('edit_user_auth_tokens_add', user_id=user_id),
180 112 {'description': 'desc', 'lifetime': -1,
181 113 'csrf_token': self.csrf_token})
182 114 assert_session_flash(response, 'Auth token successfully created')
183 115 response.follow()
184 116
185 117 # now delete our key
186 118 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
187 119 assert 3 == len(keys)
188 120
189 121 response = self.app.post(
190 122 route_path('edit_user_auth_tokens_delete', user_id=user_id),
191 123 {'del_auth_token': keys[0].user_api_key_id,
192 124 'csrf_token': self.csrf_token})
193 125
194 126 assert_session_flash(response, 'Auth token successfully deleted')
195 127 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
196 128 assert 2 == len(keys)
197 129
198 130 def test_ips(self):
199 131 self.log_user()
200 132 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
201 133 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
202 134 response.mustcontain('All IP addresses are allowed')
203 135
204 136 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
205 137 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
206 138 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
207 139 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
208 140 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
209 141 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
210 142 ('127_bad_ip', 'foobar', 'foobar', True),
211 143 ])
212 144 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
213 145 self.log_user()
214 146 user = user_util.create_user(username=test_name)
215 147 user_id = user.user_id
216 148
217 149 response = self.app.post(
218 150 route_path('edit_user_ips_add', user_id=user_id),
219 151 params={'new_ip': ip, 'csrf_token': self.csrf_token})
220 152
221 153 if failure:
222 154 assert_session_flash(
223 155 response, 'Please enter a valid IPv4 or IpV6 address')
224 156 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
225 157
226 158 response.mustcontain(no=[ip])
227 159 response.mustcontain(no=[ip_range])
228 160
229 161 else:
230 162 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
231 163 response.mustcontain(ip)
232 164 response.mustcontain(ip_range)
233 165
234 166 def test_ips_delete(self, user_util):
235 167 self.log_user()
236 168 user = user_util.create_user()
237 169 user_id = user.user_id
238 170 ip = '127.0.0.1/32'
239 171 ip_range = '127.0.0.1 - 127.0.0.1'
240 172 new_ip = UserModel().add_extra_ip(user_id, ip)
241 173 Session().commit()
242 174 new_ip_id = new_ip.ip_id
243 175
244 176 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
245 177 response.mustcontain(ip)
246 178 response.mustcontain(ip_range)
247 179
248 180 self.app.post(
249 181 route_path('edit_user_ips_delete', user_id=user_id),
250 182 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
251 183
252 184 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
253 185 response.mustcontain('All IP addresses are allowed')
254 186 response.mustcontain(no=[ip])
255 187 response.mustcontain(no=[ip_range])
256 188
257 189 def test_emails(self):
258 190 self.log_user()
259 191 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
260 192 response = self.app.get(
261 193 route_path('edit_user_emails', user_id=user.user_id))
262 194 response.mustcontain('No additional emails specified')
263 195
264 196 def test_emails_add(self, user_util):
265 197 self.log_user()
266 198 user = user_util.create_user()
267 199 user_id = user.user_id
268 200
269 201 self.app.post(
270 202 route_path('edit_user_emails_add', user_id=user_id),
271 203 params={'new_email': 'example@rhodecode.com',
272 204 'csrf_token': self.csrf_token})
273 205
274 206 response = self.app.get(
275 207 route_path('edit_user_emails', user_id=user_id))
276 208 response.mustcontain('example@rhodecode.com')
277 209
278 210 def test_emails_add_existing_email(self, user_util, user_regular):
279 211 existing_email = user_regular.email
280 212
281 213 self.log_user()
282 214 user = user_util.create_user()
283 215 user_id = user.user_id
284 216
285 217 response = self.app.post(
286 218 route_path('edit_user_emails_add', user_id=user_id),
287 219 params={'new_email': existing_email,
288 220 'csrf_token': self.csrf_token})
289 221 assert_session_flash(
290 222 response, 'This e-mail address is already taken')
291 223
292 224 response = self.app.get(
293 225 route_path('edit_user_emails', user_id=user_id))
294 226 response.mustcontain(no=[existing_email])
295 227
296 228 def test_emails_delete(self, user_util):
297 229 self.log_user()
298 230 user = user_util.create_user()
299 231 user_id = user.user_id
300 232
301 233 self.app.post(
302 234 route_path('edit_user_emails_add', user_id=user_id),
303 235 params={'new_email': 'example@rhodecode.com',
304 236 'csrf_token': self.csrf_token})
305 237
306 238 response = self.app.get(
307 239 route_path('edit_user_emails', user_id=user_id))
308 240 response.mustcontain('example@rhodecode.com')
309 241
310 242 user_email = UserEmailMap.query()\
311 243 .filter(UserEmailMap.email == 'example@rhodecode.com') \
312 244 .filter(UserEmailMap.user_id == user_id)\
313 245 .one()
314 246
315 247 del_email_id = user_email.email_id
316 248 self.app.post(
317 249 route_path('edit_user_emails_delete', user_id=user_id),
318 250 params={'del_email_id': del_email_id,
319 251 'csrf_token': self.csrf_token})
320 252
321 253 response = self.app.get(
322 254 route_path('edit_user_emails', user_id=user_id))
323 255 response.mustcontain(no=['example@rhodecode.com'])
324 256
325 257 def test_create(self, request, xhr_header):
326 258 self.log_user()
327 259 username = 'newtestuser'
328 260 password = 'test12'
329 261 password_confirmation = password
330 262 name = 'name'
331 263 lastname = 'lastname'
332 264 email = 'mail@mail.com'
333 265
334 266 self.app.get(route_path('users_new'))
335 267
336 268 response = self.app.post(route_path('users_create'), params={
337 269 'username': username,
338 270 'password': password,
339 271 'description': 'mr CTO',
340 272 'password_confirmation': password_confirmation,
341 273 'firstname': name,
342 274 'active': True,
343 275 'lastname': lastname,
344 276 'extern_name': 'rhodecode',
345 277 'extern_type': 'rhodecode',
346 278 'email': email,
347 279 'csrf_token': self.csrf_token,
348 280 })
349 281 user_link = h.link_to(
350 282 username,
351 283 route_path(
352 284 'user_edit', user_id=User.get_by_username(username).user_id))
353 285 assert_session_flash(response, 'Created user %s' % (user_link,))
354 286
355 287 @request.addfinalizer
356 288 def cleanup():
357 289 fixture.destroy_user(username)
358 290 Session().commit()
359 291
360 292 new_user = User.query().filter(User.username == username).one()
361 293
362 294 assert new_user.username == username
363 295 assert auth.check_password(password, new_user.password)
364 296 assert new_user.name == name
365 297 assert new_user.lastname == lastname
366 298 assert new_user.email == email
367 299
368 300 response = self.app.get(route_path('users_data'),
369 301 extra_environ=xhr_header)
370 302 response.mustcontain(username)
371 303
372 304 def test_create_err(self):
373 305 self.log_user()
374 306 username = 'new_user'
375 307 password = ''
376 308 name = 'name'
377 309 lastname = 'lastname'
378 310 email = 'errmail.com'
379 311
380 312 self.app.get(route_path('users_new'))
381 313
382 314 response = self.app.post(route_path('users_create'), params={
383 315 'username': username,
384 316 'password': password,
385 317 'name': name,
386 318 'active': False,
387 319 'lastname': lastname,
388 320 'description': 'mr CTO',
389 321 'email': email,
390 322 'csrf_token': self.csrf_token,
391 323 })
392 324
393 325 msg = u'Username "%(username)s" is forbidden'
394 326 msg = h.html_escape(msg % {'username': 'new_user'})
395 327 response.mustcontain('<span class="error-message">%s</span>' % msg)
396 328 response.mustcontain(
397 329 '<span class="error-message">Please enter a value</span>')
398 330 response.mustcontain(
399 331 '<span class="error-message">An email address must contain a'
400 332 ' single @</span>')
401 333
402 334 def get_user():
403 335 Session().query(User).filter(User.username == username).one()
404 336
405 337 with pytest.raises(NoResultFound):
406 338 get_user()
407 339
408 340 def test_new(self):
409 341 self.log_user()
410 342 self.app.get(route_path('users_new'))
411 343
412 344 @pytest.mark.parametrize("name, attrs", [
413 345 ('firstname', {'firstname': 'new_username'}),
414 346 ('lastname', {'lastname': 'new_username'}),
415 347 ('admin', {'admin': True}),
416 348 ('admin', {'admin': False}),
417 349 ('extern_type', {'extern_type': 'ldap'}),
418 350 ('extern_type', {'extern_type': None}),
419 351 ('extern_name', {'extern_name': 'test'}),
420 352 ('extern_name', {'extern_name': None}),
421 353 ('active', {'active': False}),
422 354 ('active', {'active': True}),
423 355 ('email', {'email': 'some@email.com'}),
424 356 ('language', {'language': 'de'}),
425 357 ('language', {'language': 'en'}),
426 358 ('description', {'description': 'hello CTO'}),
427 359 # ('new_password', {'new_password': 'foobar123',
428 360 # 'password_confirmation': 'foobar123'})
429 361 ])
430 362 def test_update(self, name, attrs, user_util):
431 363 self.log_user()
432 364 usr = user_util.create_user(
433 365 password='qweqwe',
434 366 email='testme@rhodecode.org',
435 367 extern_type='rhodecode',
436 368 extern_name='xxx',
437 369 )
438 370 user_id = usr.user_id
439 371 Session().commit()
440 372
441 373 params = usr.get_api_data()
442 374 cur_lang = params['language'] or 'en'
443 375 params.update({
444 376 'password_confirmation': '',
445 377 'new_password': '',
446 378 'language': cur_lang,
447 379 'csrf_token': self.csrf_token,
448 380 })
449 381 params.update({'new_password': ''})
450 382 params.update(attrs)
451 383 if name == 'email':
452 384 params['emails'] = [attrs['email']]
453 385 elif name == 'extern_type':
454 386 # cannot update this via form, expected value is original one
455 387 params['extern_type'] = "rhodecode"
456 388 elif name == 'extern_name':
457 389 # cannot update this via form, expected value is original one
458 390 params['extern_name'] = 'xxx'
459 391 # special case since this user is not
460 392 # logged in yet his data is not filled
461 393 # so we use creation data
462 394
463 395 response = self.app.post(
464 396 route_path('user_update', user_id=usr.user_id), params)
465 397 assert response.status_int == 302
466 398 assert_session_flash(response, 'User updated successfully')
467 399
468 400 updated_user = User.get(user_id)
469 401 updated_params = updated_user.get_api_data()
470 402 updated_params.update({'password_confirmation': ''})
471 403 updated_params.update({'new_password': ''})
472 404
473 405 del params['csrf_token']
474 406 assert params == updated_params
475 407
476 408 def test_update_and_migrate_password(
477 409 self, autologin_user, real_crypto_backend, user_util):
478 410
479 411 user = user_util.create_user()
480 412 temp_user = user.username
481 413 user.password = auth._RhodeCodeCryptoSha256().hash_create(
482 414 b'test123')
483 415 Session().add(user)
484 416 Session().commit()
485 417
486 418 params = user.get_api_data()
487 419
488 420 params.update({
489 421 'password_confirmation': 'qweqwe123',
490 422 'new_password': 'qweqwe123',
491 423 'language': 'en',
492 424 'csrf_token': autologin_user.csrf_token,
493 425 })
494 426
495 427 response = self.app.post(
496 428 route_path('user_update', user_id=user.user_id), params)
497 429 assert response.status_int == 302
498 430 assert_session_flash(response, 'User updated successfully')
499 431
500 432 # new password should be bcrypted, after log-in and transfer
501 433 user = User.get_by_username(temp_user)
502 434 assert user.password.startswith('$')
503 435
504 436 updated_user = User.get_by_username(temp_user)
505 437 updated_params = updated_user.get_api_data()
506 438 updated_params.update({'password_confirmation': 'qweqwe123'})
507 439 updated_params.update({'new_password': 'qweqwe123'})
508 440
509 441 del params['csrf_token']
510 442 assert params == updated_params
511 443
512 444 def test_delete(self):
513 445 self.log_user()
514 446 username = 'newtestuserdeleteme'
515 447
516 448 fixture.create_user(name=username)
517 449
518 450 new_user = Session().query(User)\
519 451 .filter(User.username == username).one()
520 452 response = self.app.post(
521 453 route_path('user_delete', user_id=new_user.user_id),
522 454 params={'csrf_token': self.csrf_token})
523 455
524 456 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
525 457
526 458 def test_delete_owner_of_repository(self, request, user_util):
527 459 self.log_user()
528 460 obj_name = 'test_repo'
529 461 usr = user_util.create_user()
530 462 username = usr.username
531 463 fixture.create_repo(obj_name, cur_user=usr.username)
532 464
533 465 new_user = Session().query(User)\
534 466 .filter(User.username == username).one()
535 467 response = self.app.post(
536 468 route_path('user_delete', user_id=new_user.user_id),
537 469 params={'csrf_token': self.csrf_token})
538 470
539 471 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
540 472 'Switch owners or remove those repositories:%s' % (username, obj_name)
541 473 assert_session_flash(response, msg)
542 474 fixture.destroy_repo(obj_name)
543 475
544 476 def test_delete_owner_of_repository_detaching(self, request, user_util):
545 477 self.log_user()
546 478 obj_name = 'test_repo'
547 479 usr = user_util.create_user(auto_cleanup=False)
548 480 username = usr.username
549 481 fixture.create_repo(obj_name, cur_user=usr.username)
550 482 Session().commit()
551 483
552 484 new_user = Session().query(User)\
553 485 .filter(User.username == username).one()
554 486 response = self.app.post(
555 487 route_path('user_delete', user_id=new_user.user_id),
556 488 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
557 489
558 490 msg = 'Detached 1 repositories'
559 491 assert_session_flash(response, msg)
560 492 fixture.destroy_repo(obj_name)
561 493
562 494 def test_delete_owner_of_repository_deleting(self, request, user_util):
563 495 self.log_user()
564 496 obj_name = 'test_repo'
565 497 usr = user_util.create_user(auto_cleanup=False)
566 498 username = usr.username
567 499 fixture.create_repo(obj_name, cur_user=usr.username)
568 500
569 501 new_user = Session().query(User)\
570 502 .filter(User.username == username).one()
571 503 response = self.app.post(
572 504 route_path('user_delete', user_id=new_user.user_id),
573 505 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
574 506
575 507 msg = 'Deleted 1 repositories'
576 508 assert_session_flash(response, msg)
577 509
578 510 def test_delete_owner_of_repository_group(self, request, user_util):
579 511 self.log_user()
580 512 obj_name = 'test_group'
581 513 usr = user_util.create_user()
582 514 username = usr.username
583 515 fixture.create_repo_group(obj_name, cur_user=usr.username)
584 516
585 517 new_user = Session().query(User)\
586 518 .filter(User.username == username).one()
587 519 response = self.app.post(
588 520 route_path('user_delete', user_id=new_user.user_id),
589 521 params={'csrf_token': self.csrf_token})
590 522
591 523 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
592 524 'Switch owners or remove those repository groups:%s' % (username, obj_name)
593 525 assert_session_flash(response, msg)
594 526 fixture.destroy_repo_group(obj_name)
595 527
596 528 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
597 529 self.log_user()
598 530 obj_name = 'test_group'
599 531 usr = user_util.create_user(auto_cleanup=False)
600 532 username = usr.username
601 533 fixture.create_repo_group(obj_name, cur_user=usr.username)
602 534
603 535 new_user = Session().query(User)\
604 536 .filter(User.username == username).one()
605 537 response = self.app.post(
606 538 route_path('user_delete', user_id=new_user.user_id),
607 539 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
608 540
609 541 msg = 'Deleted 1 repository groups'
610 542 assert_session_flash(response, msg)
611 543
612 544 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
613 545 self.log_user()
614 546 obj_name = 'test_group'
615 547 usr = user_util.create_user(auto_cleanup=False)
616 548 username = usr.username
617 549 fixture.create_repo_group(obj_name, cur_user=usr.username)
618 550
619 551 new_user = Session().query(User)\
620 552 .filter(User.username == username).one()
621 553 response = self.app.post(
622 554 route_path('user_delete', user_id=new_user.user_id),
623 555 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
624 556
625 557 msg = 'Detached 1 repository groups'
626 558 assert_session_flash(response, msg)
627 559 fixture.destroy_repo_group(obj_name)
628 560
629 561 def test_delete_owner_of_user_group(self, request, user_util):
630 562 self.log_user()
631 563 obj_name = 'test_user_group'
632 564 usr = user_util.create_user()
633 565 username = usr.username
634 566 fixture.create_user_group(obj_name, cur_user=usr.username)
635 567
636 568 new_user = Session().query(User)\
637 569 .filter(User.username == username).one()
638 570 response = self.app.post(
639 571 route_path('user_delete', user_id=new_user.user_id),
640 572 params={'csrf_token': self.csrf_token})
641 573
642 574 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
643 575 'Switch owners or remove those user groups:%s' % (username, obj_name)
644 576 assert_session_flash(response, msg)
645 577 fixture.destroy_user_group(obj_name)
646 578
647 579 def test_delete_owner_of_user_group_detaching(self, request, user_util):
648 580 self.log_user()
649 581 obj_name = 'test_user_group'
650 582 usr = user_util.create_user(auto_cleanup=False)
651 583 username = usr.username
652 584 fixture.create_user_group(obj_name, cur_user=usr.username)
653 585
654 586 new_user = Session().query(User)\
655 587 .filter(User.username == username).one()
656 588 try:
657 589 response = self.app.post(
658 590 route_path('user_delete', user_id=new_user.user_id),
659 591 params={'user_user_groups': 'detach',
660 592 'csrf_token': self.csrf_token})
661 593
662 594 msg = 'Detached 1 user groups'
663 595 assert_session_flash(response, msg)
664 596 finally:
665 597 fixture.destroy_user_group(obj_name)
666 598
667 599 def test_delete_owner_of_user_group_deleting(self, request, user_util):
668 600 self.log_user()
669 601 obj_name = 'test_user_group'
670 602 usr = user_util.create_user(auto_cleanup=False)
671 603 username = usr.username
672 604 fixture.create_user_group(obj_name, cur_user=usr.username)
673 605
674 606 new_user = Session().query(User)\
675 607 .filter(User.username == username).one()
676 608 response = self.app.post(
677 609 route_path('user_delete', user_id=new_user.user_id),
678 610 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
679 611
680 612 msg = 'Deleted 1 user groups'
681 613 assert_session_flash(response, msg)
682 614
683 615 def test_edit(self, user_util):
684 616 self.log_user()
685 617 user = user_util.create_user()
686 618 self.app.get(route_path('user_edit', user_id=user.user_id))
687 619
688 620 def test_edit_default_user_redirect(self):
689 621 self.log_user()
690 622 user = User.get_default_user()
691 623 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
692 624
693 625 @pytest.mark.parametrize(
694 626 'repo_create, repo_create_write, user_group_create, repo_group_create,'
695 627 'fork_create, inherit_default_permissions, expect_error,'
696 628 'expect_form_error', [
697 629 ('hg.create.none', 'hg.create.write_on_repogroup.false',
698 630 'hg.usergroup.create.false', 'hg.repogroup.create.false',
699 631 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
700 632 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
701 633 'hg.usergroup.create.false', 'hg.repogroup.create.false',
702 634 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
703 635 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
704 636 'hg.usergroup.create.true', 'hg.repogroup.create.true',
705 637 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
706 638 False),
707 639 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
708 640 'hg.usergroup.create.true', 'hg.repogroup.create.true',
709 641 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
710 642 True),
711 643 ('', '', '', '', '', '', True, False),
712 644 ])
713 645 def test_global_perms_on_user(
714 646 self, repo_create, repo_create_write, user_group_create,
715 647 repo_group_create, fork_create, expect_error, expect_form_error,
716 648 inherit_default_permissions, user_util):
717 649 self.log_user()
718 650 user = user_util.create_user()
719 651 uid = user.user_id
720 652
721 653 # ENABLE REPO CREATE ON A GROUP
722 654 perm_params = {
723 655 'inherit_default_permissions': False,
724 656 'default_repo_create': repo_create,
725 657 'default_repo_create_on_write': repo_create_write,
726 658 'default_user_group_create': user_group_create,
727 659 'default_repo_group_create': repo_group_create,
728 660 'default_fork_create': fork_create,
729 661 'default_inherit_default_permissions': inherit_default_permissions,
730 662 'csrf_token': self.csrf_token,
731 663 }
732 664 response = self.app.post(
733 665 route_path('user_edit_global_perms_update', user_id=uid),
734 666 params=perm_params)
735 667
736 668 if expect_form_error:
737 669 assert response.status_int == 200
738 670 response.mustcontain('Value must be one of')
739 671 else:
740 672 if expect_error:
741 673 msg = 'An error occurred during permissions saving'
742 674 else:
743 675 msg = 'User global permissions updated successfully'
744 676 ug = User.get(uid)
745 677 del perm_params['inherit_default_permissions']
746 678 del perm_params['csrf_token']
747 679 assert perm_params == ug.get_default_perms()
748 680 assert_session_flash(response, msg)
749 681
750 682 def test_global_permissions_initial_values(self, user_util):
751 683 self.log_user()
752 684 user = user_util.create_user()
753 685 uid = user.user_id
754 686 response = self.app.get(
755 687 route_path('user_edit_global_perms', user_id=uid))
756 688 default_user = User.get_default_user()
757 689 default_permissions = default_user.get_default_perms()
758 690 assert_response = response.assert_response()
759 691 expected_permissions = (
760 692 'default_repo_create', 'default_repo_create_on_write',
761 693 'default_fork_create', 'default_repo_group_create',
762 694 'default_user_group_create', 'default_inherit_default_permissions')
763 695 for permission in expected_permissions:
764 696 css_selector = '[name={}][checked=checked]'.format(permission)
765 697 element = assert_response.get_element(css_selector)
766 698 assert element.value == default_permissions[permission]
767 699
768 700 def test_perms_summary_page(self):
769 701 user = self.log_user()
770 702 response = self.app.get(
771 703 route_path('edit_user_perms_summary', user_id=user['user_id']))
772 704 for repo in Repository.query().all():
773 705 response.mustcontain(repo.repo_name)
774 706
775 707 def test_perms_summary_page_json(self):
776 708 user = self.log_user()
777 709 response = self.app.get(
778 710 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
779 711 for repo in Repository.query().all():
780 712 response.mustcontain(repo.repo_name)
781 713
782 714 def test_audit_log_page(self):
783 715 user = self.log_user()
784 716 self.app.get(
785 717 route_path('edit_user_audit_logs', user_id=user['user_id']))
786 718
787 719 def test_audit_log_page_download(self):
788 720 user = self.log_user()
789 721 user_id = user['user_id']
790 722 response = self.app.get(
791 723 route_path('edit_user_audit_logs_download', user_id=user_id))
792 724
793 725 assert response.content_disposition == \
794 726 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
795 727 assert response.content_type == "application/json"
@@ -1,177 +1,155 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.model.db import User, UserSshKeys
23 23
24 24 from rhodecode.tests import TestController, assert_session_flash
25 25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.routes import route_path
26 27
27 28 fixture = Fixture()
28 29
29 30
30 def route_path(name, params=None, **kwargs):
31 import urllib.request
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'edit_user_ssh_keys':
38 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
39 'edit_user_ssh_keys_generate_keypair':
40 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
41 'edit_user_ssh_keys_add':
42 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
43 'edit_user_ssh_keys_delete':
44 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
45
46 }[name].format(**kwargs)
47
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 return base_url
51
52
53 31 class TestAdminUsersSshKeysView(TestController):
54 32 INVALID_KEY = """\
55 33 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
56 34 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
57 35 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
58 36 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
59 37 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
60 38 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
61 39 your_email@example.com
62 40 """
63 41 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
64 42 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
65 43 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
66 44 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
67 45 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
68 46 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
69 47 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
70 48 'your_email@example.com'
71 49 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
72 50
73 51 def test_ssh_keys_default_user(self):
74 52 self.log_user()
75 53 user = User.get_default_user()
76 54 self.app.get(
77 55 route_path('edit_user_ssh_keys', user_id=user.user_id),
78 56 status=302)
79 57
80 58 def test_add_ssh_key_error(self, user_util):
81 59 self.log_user()
82 60 user = user_util.create_user()
83 61 user_id = user.user_id
84 62
85 63 key_data = self.INVALID_KEY
86 64
87 65 desc = 'MY SSH KEY'
88 66 response = self.app.post(
89 67 route_path('edit_user_ssh_keys_add', user_id=user_id),
90 68 {'description': desc, 'key_data': key_data,
91 69 'csrf_token': self.csrf_token})
92 70 assert_session_flash(response, 'An error occurred during ssh '
93 71 'key saving: Unable to decode the key')
94 72
95 73 def test_ssh_key_duplicate(self, user_util):
96 74 self.log_user()
97 75 user = user_util.create_user()
98 76 user_id = user.user_id
99 77
100 78 key_data = self.VALID_KEY
101 79
102 80 desc = 'MY SSH KEY'
103 81 response = self.app.post(
104 82 route_path('edit_user_ssh_keys_add', user_id=user_id),
105 83 {'description': desc, 'key_data': key_data,
106 84 'csrf_token': self.csrf_token})
107 85 assert_session_flash(response, 'Ssh Key successfully created')
108 86 response.follow() # flush session flash
109 87
110 88 # add the same key AGAIN
111 89 desc = 'MY SSH KEY'
112 90 response = self.app.post(
113 91 route_path('edit_user_ssh_keys_add', user_id=user_id),
114 92 {'description': desc, 'key_data': key_data,
115 93 'csrf_token': self.csrf_token})
116 94
117 95 err = 'Such key with fingerprint `{}` already exists, ' \
118 96 'please use a different one'.format(self.FINGERPRINT)
119 97 assert_session_flash(response, 'An error occurred during ssh key '
120 98 'saving: {}'.format(err))
121 99
122 100 def test_add_ssh_key(self, user_util):
123 101 self.log_user()
124 102 user = user_util.create_user()
125 103 user_id = user.user_id
126 104
127 105 key_data = self.VALID_KEY
128 106
129 107 desc = 'MY SSH KEY'
130 108 response = self.app.post(
131 109 route_path('edit_user_ssh_keys_add', user_id=user_id),
132 110 {'description': desc, 'key_data': key_data,
133 111 'csrf_token': self.csrf_token})
134 112 assert_session_flash(response, 'Ssh Key successfully created')
135 113
136 114 response = response.follow()
137 115 response.mustcontain(desc)
138 116
139 117 def test_delete_ssh_key(self, user_util):
140 118 self.log_user()
141 119 user = user_util.create_user()
142 120 user_id = user.user_id
143 121
144 122 key_data = self.VALID_KEY
145 123
146 124 desc = 'MY SSH KEY'
147 125 response = self.app.post(
148 126 route_path('edit_user_ssh_keys_add', user_id=user_id),
149 127 {'description': desc, 'key_data': key_data,
150 128 'csrf_token': self.csrf_token})
151 129 assert_session_flash(response, 'Ssh Key successfully created')
152 130 response = response.follow() # flush the Session flash
153 131
154 132 # now delete our key
155 133 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
156 134 assert 1 == len(keys)
157 135
158 136 response = self.app.post(
159 137 route_path('edit_user_ssh_keys_delete', user_id=user_id),
160 138 {'del_ssh_key': keys[0].ssh_key_id,
161 139 'csrf_token': self.csrf_token})
162 140
163 141 assert_session_flash(response, 'Ssh key successfully deleted')
164 142 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
165 143 assert 0 == len(keys)
166 144
167 145 def test_generate_keypair(self, user_util):
168 146 self.log_user()
169 147 user = user_util.create_user()
170 148 user_id = user.user_id
171 149
172 150 response = self.app.get(
173 151 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
174 152
175 153 response.mustcontain('Private key')
176 154 response.mustcontain('Public key')
177 155 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,261 +1,246 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import os
19 19 import pytest
20 20
21 21 from rhodecode.lib.ext_json import json
22 22 from rhodecode.model.auth_token import AuthTokenModel
23 23 from rhodecode.model.db import Session, FileStore, Repository, User
24 from rhodecode.tests import TestController
25 24 from rhodecode.apps.file_store import utils, config_keys
26 25
27
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
32
33 base_url = {
34 'upload_file': '/_file_store/upload',
35 'download_file': '/_file_store/download/{fid}',
36 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
37
38 }[name].format(**kwargs)
39
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
26 from rhodecode.tests import TestController
27 from rhodecode.tests.routes import route_path
43 28
44 29
45 30 class TestFileStoreViews(TestController):
46 31
47 32 @pytest.mark.parametrize("fid, content, exists", [
48 33 ('abcde-0.jpg', "xxxxx", True),
49 34 ('abcde-0.exe', "1234567", True),
50 35 ('abcde-0.jpg', "xxxxx", False),
51 36 ])
52 37 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 38 user = self.log_user()
54 39 user_id = user['user_id']
55 40 repo_id = user_util.create_repo().repo_id
56 41 store_path = self.app._pyramid_settings[config_keys.store_path]
57 42 store_uid = fid
58 43
59 44 if exists:
60 45 status = 200
61 46 store = utils.get_file_storage({config_keys.store_path: store_path})
62 47 filesystem_file = os.path.join(str(tmpdir), fid)
63 48 with open(filesystem_file, 'wt') as f:
64 49 f.write(content)
65 50
66 51 with open(filesystem_file, 'rb') as f:
67 52 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68 53
69 54 entry = FileStore.create(
70 55 file_uid=store_uid, filename=metadata["filename"],
71 56 file_hash=metadata["sha256"], file_size=metadata["size"],
72 57 file_display_name='file_display_name',
73 58 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 59 check_acl=True, user_id=user_id,
75 60 scope_repo_id=repo_id
76 61 )
77 62 Session().add(entry)
78 63 Session().commit()
79 64
80 65 else:
81 66 status = 404
82 67
83 68 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84 69
85 70 if exists:
86 71 assert response.text == content
87 72 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 73 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 74 assert os.path.exists(metadata_file)
90 75 with open(metadata_file, 'rb') as f:
91 76 json_data = json.loads(f.read())
92 77
93 78 assert json_data
94 79 assert 'size' in json_data
95 80
96 81 def test_upload_files_without_content_to_store(self):
97 82 self.log_user()
98 83 response = self.app.post(
99 84 route_path('upload_file'),
100 85 params={'csrf_token': self.csrf_token},
101 86 status=200)
102 87
103 88 assert response.json == {
104 89 'error': 'store_file data field is missing',
105 90 'access_path': None,
106 91 'store_fid': None}
107 92
108 93 def test_upload_files_bogus_content_to_store(self):
109 94 self.log_user()
110 95 response = self.app.post(
111 96 route_path('upload_file'),
112 97 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 98 status=200)
114 99
115 100 assert response.json == {
116 101 'error': 'filename cannot be read from the data field',
117 102 'access_path': None,
118 103 'store_fid': None}
119 104
120 105 def test_upload_content_to_store(self):
121 106 self.log_user()
122 107 response = self.app.post(
123 108 route_path('upload_file'),
124 109 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
125 110 params={'csrf_token': self.csrf_token},
126 111 status=200)
127 112
128 113 assert response.json['store_fid']
129 114
130 115 @pytest.fixture()
131 116 def create_artifact_factory(self, tmpdir):
132 117 def factory(user_id, content):
133 118 store_path = self.app._pyramid_settings[config_keys.store_path]
134 119 store = utils.get_file_storage({config_keys.store_path: store_path})
135 120 fid = 'example.txt'
136 121
137 122 filesystem_file = os.path.join(str(tmpdir), fid)
138 123 with open(filesystem_file, 'wt') as f:
139 124 f.write(content)
140 125
141 126 with open(filesystem_file, 'rb') as f:
142 127 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143 128
144 129 entry = FileStore.create(
145 130 file_uid=store_uid, filename=metadata["filename"],
146 131 file_hash=metadata["sha256"], file_size=metadata["size"],
147 132 file_display_name='file_display_name',
148 133 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 134 check_acl=True, user_id=user_id,
150 135 )
151 136 Session().add(entry)
152 137 Session().commit()
153 138 return entry
154 139 return factory
155 140
156 141 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 142 user = self.log_user()
158 143 user_id = user['user_id']
159 144 content = 'HELLO MY NAME IS ARTIFACT !'
160 145
161 146 artifact = create_artifact_factory(user_id, content)
162 147 file_uid = artifact.file_uid
163 148 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 149 assert response.text == content
165 150
166 151 # log-in to new user and test download again
167 152 user = user_util.create_user(password='qweqwe')
168 153 self.log_user(user.username, 'qweqwe')
169 154 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 155 assert response.text == content
171 156
172 157 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 158 user = self.log_user()
174 159 user_id = user['user_id']
175 160 content = 'HELLO MY NAME IS ARTIFACT !'
176 161
177 162 artifact = create_artifact_factory(user_id, content)
178 163 # bind to repo
179 164 repo = user_util.create_repo()
180 165 repo_id = repo.repo_id
181 166 artifact.scope_repo_id = repo_id
182 167 Session().add(artifact)
183 168 Session().commit()
184 169
185 170 file_uid = artifact.file_uid
186 171 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 172 assert response.text == content
188 173
189 174 # log-in to new user and test download again
190 175 user = user_util.create_user(password='qweqwe')
191 176 self.log_user(user.username, 'qweqwe')
192 177 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 178 assert response.text == content
194 179
195 180 # forbid user the rights to repo
196 181 repo = Repository.get(repo_id)
197 182 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 183 self.app.get(route_path('download_file', fid=file_uid), status=404)
199 184
200 185 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 186 user = self.log_user()
202 187 user_id = user['user_id']
203 188 content = 'HELLO MY NAME IS ARTIFACT !'
204 189
205 190 artifact = create_artifact_factory(user_id, content)
206 191 # bind to user
207 192 user = user_util.create_user(password='qweqwe')
208 193
209 194 artifact.scope_user_id = user.user_id
210 195 Session().add(artifact)
211 196 Session().commit()
212 197
213 198 # artifact creator doesn't have access since it's bind to another user
214 199 file_uid = artifact.file_uid
215 200 self.app.get(route_path('download_file', fid=file_uid), status=404)
216 201
217 202 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 203 self.log_user(user.username, 'qweqwe')
219 204 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 205 assert response.text == content
221 206
222 207 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 208 user_id = User.get_first_super_admin().user_id
224 209 content = 'HELLO MY NAME IS ARTIFACT !'
225 210
226 211 artifact = create_artifact_factory(user_id, content)
227 212 # bind to repo
228 213 repo = user_util.create_repo()
229 214 repo_id = repo.repo_id
230 215 artifact.scope_repo_id = repo_id
231 216 Session().add(artifact)
232 217 Session().commit()
233 218
234 219 file_uid = artifact.file_uid
235 220 self.app.get(route_path('download_file_by_token',
236 221 _auth_token='bogus', fid=file_uid), status=302)
237 222
238 223 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 224 user = User.get_first_super_admin()
240 225 AuthTokenModel().create(user, 'test artifact token',
241 226 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242 227
243 228 user = User.get_first_super_admin()
244 229 artifact_token = user.artifact_token
245 230
246 231 user_id = User.get_first_super_admin().user_id
247 232 content = 'HELLO MY NAME IS ARTIFACT !'
248 233
249 234 artifact = create_artifact_factory(user_id, content)
250 235 # bind to repo
251 236 repo = user_util.create_repo()
252 237 repo_id = repo.repo_id
253 238 artifact.scope_repo_id = repo_id
254 239 Session().add(artifact)
255 240 Session().commit()
256 241
257 242 file_uid = artifact.file_uid
258 243 response = self.app.get(
259 244 route_path('download_file_by_token',
260 245 _auth_token=artifact_token, fid=file_uid), status=200)
261 246 assert response.text == content
@@ -1,389 +1,365 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import mock
20 20 import pytest
21 21
22 22 from rhodecode.lib import helpers as h
23 23 from rhodecode.model.db import User, Gist
24 24 from rhodecode.model.gist import GistModel
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.tests import (
27 27 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
28 28 TestController, assert_session_flash)
29
30
31 def route_path(name, params=None, **kwargs):
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'gists_show': ADMIN_PREFIX + '/gists',
38 'gists_new': ADMIN_PREFIX + '/gists/new',
39 'gists_create': ADMIN_PREFIX + '/gists/create',
40 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
41 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
42 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
43 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
44 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
45 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
46 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
47 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
48
49 }[name].format(**kwargs)
50
51 if params:
52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 return base_url
29 from rhodecode.tests.routes import route_path
54 30
55 31
56 32 class GistUtility(object):
57 33
58 34 def __init__(self):
59 35 self._gist_ids = []
60 36
61 37 def __call__(
62 38 self, f_name: bytes, content: bytes = b'some gist', lifetime=-1,
63 39 description='gist-desc', gist_type='public',
64 40 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 41 gist_mapping = {
66 42 f_name: {'content': content}
67 43 }
68 44 user = User.get_by_username(owner)
69 45 gist = GistModel().create(
70 46 description, owner=user, gist_mapping=gist_mapping,
71 47 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 48 Session().commit()
73 49 self._gist_ids.append(gist.gist_id)
74 50 return gist
75 51
76 52 def cleanup(self):
77 53 for gist_id in self._gist_ids:
78 54 gist = Gist.get(gist_id)
79 55 if gist:
80 56 Session().delete(gist)
81 57
82 58 Session().commit()
83 59
84 60
85 61 @pytest.fixture()
86 62 def create_gist(request):
87 63 gist_utility = GistUtility()
88 64 request.addfinalizer(gist_utility.cleanup)
89 65 return gist_utility
90 66
91 67
92 68 class TestGistsController(TestController):
93 69
94 70 def test_index_empty(self, create_gist):
95 71 self.log_user()
96 72 response = self.app.get(route_path('gists_show'))
97 73 response.mustcontain('var gist_data = [];')
98 74
99 75 def test_index(self, create_gist):
100 76 self.log_user()
101 77 g1 = create_gist(b'gist1')
102 78 g2 = create_gist(b'gist2', lifetime=1400)
103 79 g3 = create_gist(b'gist3', description='gist3-desc')
104 80 g4 = create_gist(b'gist4', gist_type='private').gist_access_id
105 81 response = self.app.get(route_path('gists_show'))
106 82
107 83 response.mustcontain(g1.gist_access_id)
108 84 response.mustcontain(g2.gist_access_id)
109 85 response.mustcontain(g3.gist_access_id)
110 86 response.mustcontain('gist3-desc')
111 87 response.mustcontain(no=[g4])
112 88
113 89 # Expiration information should be visible
114 90 expires_tag = str(h.age_component(h.time_to_utcdatetime(g2.gist_expires)))
115 91 response.mustcontain(expires_tag.replace('"', '\\"'))
116 92
117 93 def test_index_private_gists(self, create_gist):
118 94 self.log_user()
119 95 gist = create_gist(b'gist5', gist_type='private')
120 96 response = self.app.get(route_path('gists_show', params=dict(private=1)))
121 97
122 98 # and privates
123 99 response.mustcontain(gist.gist_access_id)
124 100
125 101 def test_index_show_all(self, create_gist):
126 102 self.log_user()
127 103 create_gist(b'gist1')
128 104 create_gist(b'gist2', lifetime=1400)
129 105 create_gist(b'gist3', description='gist3-desc')
130 106 create_gist(b'gist4', gist_type='private')
131 107
132 108 response = self.app.get(route_path('gists_show', params=dict(all=1)))
133 109
134 110 assert len(GistModel.get_all()) == 4
135 111 # and privates
136 112 for gist in GistModel.get_all():
137 113 response.mustcontain(gist.gist_access_id)
138 114
139 115 def test_index_show_all_hidden_from_regular(self, create_gist):
140 116 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
141 117 create_gist(b'gist2', gist_type='private')
142 118 create_gist(b'gist3', gist_type='private')
143 119 create_gist(b'gist4', gist_type='private')
144 120
145 121 response = self.app.get(route_path('gists_show', params=dict(all=1)))
146 122
147 123 assert len(GistModel.get_all()) == 3
148 124 # since we don't have access to private in this view, we
149 125 # should see nothing
150 126 for gist in GistModel.get_all():
151 127 response.mustcontain(no=[gist.gist_access_id])
152 128
153 129 def test_create(self):
154 130 self.log_user()
155 131 response = self.app.post(
156 132 route_path('gists_create'),
157 133 params={'lifetime': -1,
158 134 'content': 'gist test',
159 135 'filename': 'foo',
160 136 'gist_type': 'public',
161 137 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
162 138 'csrf_token': self.csrf_token},
163 139 status=302)
164 140 response = response.follow()
165 141 response.mustcontain('added file: foo')
166 142 response.mustcontain('gist test')
167 143
168 144 def test_create_with_path_with_dirs(self):
169 145 self.log_user()
170 146 response = self.app.post(
171 147 route_path('gists_create'),
172 148 params={'lifetime': -1,
173 149 'content': 'gist test',
174 150 'filename': '/home/foo',
175 151 'gist_type': 'public',
176 152 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
177 153 'csrf_token': self.csrf_token},
178 154 status=200)
179 155 response.mustcontain('Filename /home/foo cannot be inside a directory')
180 156
181 157 def test_access_expired_gist(self, create_gist):
182 158 self.log_user()
183 159 gist = create_gist(b'never-see-me')
184 160 gist.gist_expires = 0 # 1970
185 161 Session().add(gist)
186 162 Session().commit()
187 163
188 164 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
189 165 status=404)
190 166
191 167 def test_create_private(self):
192 168 self.log_user()
193 169 response = self.app.post(
194 170 route_path('gists_create'),
195 171 params={'lifetime': -1,
196 172 'content': 'private gist test',
197 173 'filename': 'private-foo',
198 174 'gist_type': 'private',
199 175 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
200 176 'csrf_token': self.csrf_token},
201 177 status=302)
202 178 response = response.follow()
203 179 response.mustcontain('added file: private-foo<')
204 180 response.mustcontain('private gist test')
205 181 response.mustcontain('Private Gist')
206 182 # Make sure private gists are not indexed by robots
207 183 response.mustcontain(
208 184 '<meta name="robots" content="noindex, nofollow">')
209 185
210 186 def test_create_private_acl_private(self):
211 187 self.log_user()
212 188 response = self.app.post(
213 189 route_path('gists_create'),
214 190 params={'lifetime': -1,
215 191 'content': 'private gist test',
216 192 'filename': 'private-foo',
217 193 'gist_type': 'private',
218 194 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
219 195 'csrf_token': self.csrf_token},
220 196 status=302)
221 197 response = response.follow()
222 198 response.mustcontain('added file: private-foo<')
223 199 response.mustcontain('private gist test')
224 200 response.mustcontain('Private Gist')
225 201 # Make sure private gists are not indexed by robots
226 202 response.mustcontain(
227 203 '<meta name="robots" content="noindex, nofollow">')
228 204
229 205 def test_create_with_description(self):
230 206 self.log_user()
231 207 response = self.app.post(
232 208 route_path('gists_create'),
233 209 params={'lifetime': -1,
234 210 'content': 'gist test',
235 211 'filename': 'foo-desc',
236 212 'description': 'gist-desc',
237 213 'gist_type': 'public',
238 214 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
239 215 'csrf_token': self.csrf_token},
240 216 status=302)
241 217 response = response.follow()
242 218 response.mustcontain('added file: foo-desc')
243 219 response.mustcontain('gist test')
244 220 response.mustcontain('gist-desc')
245 221
246 222 def test_create_public_with_anonymous_access(self):
247 223 self.log_user()
248 224 params = {
249 225 'lifetime': -1,
250 226 'content': 'gist test',
251 227 'filename': 'foo-desc',
252 228 'description': 'gist-desc',
253 229 'gist_type': 'public',
254 230 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
255 231 'csrf_token': self.csrf_token
256 232 }
257 233 response = self.app.post(
258 234 route_path('gists_create'), params=params, status=302)
259 235 self.logout_user()
260 236 response = response.follow()
261 237 response.mustcontain('added file: foo-desc')
262 238 response.mustcontain('gist test')
263 239 response.mustcontain('gist-desc')
264 240
265 241 def test_new(self):
266 242 self.log_user()
267 243 self.app.get(route_path('gists_new'))
268 244
269 245 def test_delete(self, create_gist):
270 246 self.log_user()
271 247 gist = create_gist(b'delete-me')
272 248 response = self.app.post(
273 249 route_path('gist_delete', gist_id=gist.gist_id),
274 250 params={'csrf_token': self.csrf_token})
275 251 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
276 252
277 253 def test_delete_normal_user_his_gist(self, create_gist):
278 254 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
279 255 gist = create_gist(b'delete-me', owner=TEST_USER_REGULAR_LOGIN)
280 256
281 257 response = self.app.post(
282 258 route_path('gist_delete', gist_id=gist.gist_id),
283 259 params={'csrf_token': self.csrf_token})
284 260 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
285 261
286 262 def test_delete_normal_user_not_his_own_gist(self, create_gist):
287 263 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
288 264 gist = create_gist(b'delete-me-2')
289 265
290 266 self.app.post(
291 267 route_path('gist_delete', gist_id=gist.gist_id),
292 268 params={'csrf_token': self.csrf_token}, status=404)
293 269
294 270 def test_show(self, create_gist):
295 271 gist = create_gist(b'gist-show-me')
296 272 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
297 273
298 274 response.mustcontain('added file: gist-show-me<')
299 275
300 276 assert_response = response.assert_response()
301 277 assert_response.element_equals_to(
302 278 'div.rc-user span.user',
303 279 '<a href="/_profiles/test_admin">test_admin</a>')
304 280
305 281 response.mustcontain('gist-desc')
306 282
307 283 def test_show_without_hg(self, create_gist):
308 284 with mock.patch(
309 285 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
310 286 gist = create_gist(b'gist-show-me-again')
311 287 self.app.get(
312 288 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
313 289
314 290 def test_show_acl_private(self, create_gist):
315 291 gist = create_gist(b'gist-show-me-only-when-im-logged-in',
316 292 acl_level=Gist.ACL_LEVEL_PRIVATE)
317 293 self.app.get(
318 294 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
319 295
320 296 # now we log-in we should see thi gist
321 297 self.log_user()
322 298 response = self.app.get(
323 299 route_path('gist_show', gist_id=gist.gist_access_id))
324 300 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
325 301
326 302 assert_response = response.assert_response()
327 303 assert_response.element_equals_to(
328 304 'div.rc-user span.user',
329 305 '<a href="/_profiles/test_admin">test_admin</a>')
330 306 response.mustcontain('gist-desc')
331 307
332 308 def test_show_as_raw(self, create_gist):
333 309 gist = create_gist(b'gist-show-me', content=b'GIST CONTENT')
334 310 response = self.app.get(
335 311 route_path('gist_show_formatted',
336 312 gist_id=gist.gist_access_id, revision='tip',
337 313 format='raw'))
338 314 assert response.text == 'GIST CONTENT'
339 315
340 316 def test_show_as_raw_individual_file(self, create_gist):
341 317 gist = create_gist(b'gist-show-me-raw', content=b'GIST BODY')
342 318 response = self.app.get(
343 319 route_path('gist_show_formatted_path',
344 320 gist_id=gist.gist_access_id, format='raw',
345 321 revision='tip', f_path='gist-show-me-raw'))
346 322 assert response.text == 'GIST BODY'
347 323
348 324 def test_edit_page(self, create_gist):
349 325 self.log_user()
350 326 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
351 327 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
352 328 response.mustcontain('GIST EDIT BODY')
353 329
354 330 def test_edit_page_non_logged_user(self, create_gist):
355 331 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
356 332 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
357 333 status=302)
358 334
359 335 def test_edit_normal_user_his_gist(self, create_gist):
360 336 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
361 337 gist = create_gist(b'gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
362 338 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
363 339 status=200))
364 340
365 341 def test_edit_normal_user_not_his_own_gist(self, create_gist):
366 342 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
367 343 gist = create_gist(b'delete-me')
368 344 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
369 345 status=404)
370 346
371 347 def test_user_first_name_is_escaped(self, user_util, create_gist):
372 348 xss_atack_string = '"><script>alert(\'First Name\')</script>'
373 349 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
374 350 password = 'test'
375 351 user = user_util.create_user(
376 352 firstname=xss_atack_string, password=password)
377 353 create_gist(b'gist', gist_type='public', owner=user.username)
378 354 response = self.app.get(route_path('gists_show'))
379 355 response.mustcontain(xss_escaped_string)
380 356
381 357 def test_user_last_name_is_escaped(self, user_util, create_gist):
382 358 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
383 359 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
384 360 password = 'test'
385 361 user = user_util.create_user(
386 362 lastname=xss_atack_string, password=password)
387 363 create_gist(b'gist', gist_type='public', owner=user.username)
388 364 response = self.app.get(route_path('gists_show'))
389 365 response.mustcontain(xss_escaped_string)
@@ -1,179 +1,167 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from . import assert_and_get_main_filter_content
22 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
23 from rhodecode.tests.fixture import Fixture
24 22
25 23 from rhodecode.lib.utils import map_groups
26 24 from rhodecode.lib.ext_json import json
27 25 from rhodecode.model.repo import RepoModel
28 26 from rhodecode.model.repo_group import RepoGroupModel
29 27 from rhodecode.model.db import Session, Repository, RepoGroup
30 28
31 fixture = Fixture()
32
33
34 def route_path(name, params=None, **kwargs):
35 import urllib.request
36 import urllib.parse
37 import urllib.error
29 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
30 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.routes import route_path
38 32
39 base_url = {
40 'goto_switcher_data': '/_goto_data',
41 }[name].format(**kwargs)
42
43 if params:
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 return base_url
33 fixture = Fixture()
46 34
47 35
48 36 class TestGotoSwitcherData(TestController):
49 37
50 38 required_repos_with_groups = [
51 39 'abc',
52 40 'abc-fork',
53 41 'forks/abcd',
54 42 'abcd',
55 43 'abcde',
56 44 'a/abc',
57 45 'aa/abc',
58 46 'aaa/abc',
59 47 'aaaa/abc',
60 48 'repos_abc/aaa/abc',
61 49 'abc_repos/abc',
62 50 'abc_repos/abcd',
63 51 'xxx/xyz',
64 52 'forked-abc/a/abc'
65 53 ]
66 54
67 55 @pytest.fixture(autouse=True, scope='class')
68 56 def prepare(self, request, baseapp):
69 57 for repo_and_group in self.required_repos_with_groups:
70 58 # create structure of groups and return the last group
71 59
72 60 repo_group = map_groups(repo_and_group)
73 61
74 62 RepoModel()._create_repo(
75 63 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
76 64 repo_group=getattr(repo_group, 'group_id', None))
77 65
78 66 Session().commit()
79 67
80 68 request.addfinalizer(self.cleanup)
81 69
82 70 def cleanup(self):
83 71 # first delete all repos
84 72 for repo_and_groups in self.required_repos_with_groups:
85 73 repo = Repository.get_by_repo_name(repo_and_groups)
86 74 if repo:
87 75 RepoModel().delete(repo)
88 76 Session().commit()
89 77
90 78 # then delete all empty groups
91 79 for repo_and_groups in self.required_repos_with_groups:
92 80 if '/' in repo_and_groups:
93 81 r_group = repo_and_groups.rsplit('/', 1)[0]
94 82 repo_group = RepoGroup.get_by_group_name(r_group)
95 83 if not repo_group:
96 84 continue
97 85 parents = repo_group.parents
98 86 RepoGroupModel().delete(repo_group, force_delete=True)
99 87 Session().commit()
100 88
101 89 for el in reversed(parents):
102 90 RepoGroupModel().delete(el, force_delete=True)
103 91 Session().commit()
104 92
105 93 def test_empty_query(self, xhr_header):
106 94 self.log_user()
107 95
108 96 response = self.app.get(
109 97 route_path('goto_switcher_data'),
110 98 extra_environ=xhr_header, status=200)
111 99 result = json.loads(response.body)['suggestions']
112 100
113 101 assert result == []
114 102
115 103 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
116 104 self.log_user()
117 105
118 106 response = self.app.get(
119 107 route_path('goto_switcher_data'),
120 108 params={'query': 'abc'},
121 109 extra_environ=xhr_header, status=200)
122 110 result = json.loads(response.body)['suggestions']
123 111
124 112 repos, groups, users, commits = assert_and_get_main_filter_content(result)
125 113
126 114 assert len(repos) == 13
127 115 assert len(groups) == 5
128 116 assert len(users) == 0
129 117 assert len(commits) == 0
130 118
131 119 def test_returns_list_of_users_filtered(self, xhr_header):
132 120 self.log_user()
133 121
134 122 response = self.app.get(
135 123 route_path('goto_switcher_data'),
136 124 params={'query': 'user:admin'},
137 125 extra_environ=xhr_header, status=200)
138 126 result = json.loads(response.body)['suggestions']
139 127
140 128 repos, groups, users, commits = assert_and_get_main_filter_content(result)
141 129
142 130 assert len(repos) == 0
143 131 assert len(groups) == 0
144 132 assert len(users) == 1
145 133 assert len(commits) == 0
146 134
147 135 def test_returns_list_of_commits_filtered(self, xhr_header):
148 136 self.log_user()
149 137
150 138 response = self.app.get(
151 139 route_path('goto_switcher_data'),
152 140 params={'query': 'commit:e8'},
153 141 extra_environ=xhr_header, status=200)
154 142 result = json.loads(response.body)['suggestions']
155 143
156 144 repos, groups, users, commits = assert_and_get_main_filter_content(result)
157 145
158 146 assert len(repos) == 0
159 147 assert len(groups) == 0
160 148 assert len(users) == 0
161 149 assert len(commits) == 5
162 150
163 151 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
164 152 self.log_user()
165 153
166 154 response = self.app.get(
167 155 route_path('goto_switcher_data'),
168 156 params={'query': 'abc'},
169 157 extra_environ=xhr_header, status=200)
170 158 result = json.loads(response.body)['suggestions']
171 159
172 160 repos, groups, users, commits = assert_and_get_main_filter_content(result)
173 161
174 162 test_repos = [x['value_display'] for x in repos[:4]]
175 163 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
176 164
177 165 test_groups = [x['value_display'] for x in groups[:4]]
178 166 assert ['abc_repos', 'repos_abc',
179 167 'forked-abc', 'forked-abc/a'] == test_groups
@@ -1,95 +1,83 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 from . import assert_and_get_repo_list_content
20 from rhodecode.tests import TestController
21 from rhodecode.tests.fixture import Fixture
20
22 21 from rhodecode.model.db import Repository
23 22 from rhodecode.lib.ext_json import json
24 23
24 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.routes import route_path
25 27
26 28 fixture = Fixture()
27 29
28 30
29 def route_path(name, params=None, **kwargs):
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'repo_list_data': '/_repos',
36 }[name].format(**kwargs)
37
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
41
42
43 31 class TestRepoListData(TestController):
44 32
45 33 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 34 self.log_user()
47 35
48 36 response = self.app.get(
49 37 route_path('repo_list_data'),
50 38 extra_environ=xhr_header, status=200)
51 39 result = json.loads(response.body)['results']
52 40
53 41 repos = assert_and_get_repo_list_content(result)
54 42
55 43 assert len(repos) == len(Repository.get_all())
56 44
57 45 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 46 self.log_user()
59 47
60 48 response = self.app.get(
61 49 route_path('repo_list_data'),
62 50 params={'query': 'vcs_test_git'},
63 51 extra_environ=xhr_header, status=200)
64 52 result = json.loads(response.body)['results']
65 53
66 54 repos = assert_and_get_repo_list_content(result)
67 55
68 56 assert len(repos) == len(Repository.query().filter(
69 57 Repository.repo_name.ilike('%vcs_test_git%')).all())
70 58
71 59 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 60 self.log_user()
73 61
74 62 response = self.app.get(
75 63 route_path('repo_list_data'),
76 64 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 65 extra_environ=xhr_header, status=200)
78 66 result = json.loads(response.body)['results']
79 67
80 68 repos = assert_and_get_repo_list_content(result)
81 69
82 70 assert len(repos) == len(Repository.query().filter(
83 71 Repository.repo_name.ilike('%vcs_test_git%')).all())
84 72
85 73 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 74 self.log_user()
87 75 response = self.app.get(
88 76 route_path('repo_list_data'),
89 77 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 78 extra_environ=xhr_header, status=200)
91 79 result = json.loads(response.body)['results']
92 80
93 81 repos = assert_and_get_repo_list_content(result)
94 82
95 83 assert len(repos) == 0
@@ -1,110 +1,97 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import pytest
19 19
20 from rhodecode.lib.ext_json import json
21
20 22 from rhodecode.tests import TestController
21 23 from rhodecode.tests.fixture import Fixture
22 from rhodecode.lib.ext_json import json
24 from rhodecode.tests.routes import route_path
23 25
24 26 fixture = Fixture()
25 27
26 28
27 def route_path(name, params=None, **kwargs):
28 import urllib.request
29 import urllib.parse
30 import urllib.error
31
32 base_url = {
33 'user_autocomplete_data': '/_users',
34 'user_group_autocomplete_data': '/_user_groups'
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
40
41
42 29 class TestUserAutocompleteData(TestController):
43 30
44 31 def test_returns_list_of_users(self, user_util, xhr_header):
45 32 self.log_user()
46 33 user = user_util.create_user(active=True)
47 34 user_name = user.username
48 35 response = self.app.get(
49 36 route_path('user_autocomplete_data'),
50 37 extra_environ=xhr_header, status=200)
51 38 result = json.loads(response.body)
52 39 values = [suggestion['value'] for suggestion in result['suggestions']]
53 40 assert user_name in values
54 41
55 42 def test_returns_inactive_users_when_active_flag_sent(
56 43 self, user_util, xhr_header):
57 44 self.log_user()
58 45 user = user_util.create_user(active=False)
59 46 user_name = user.username
60 47
61 48 response = self.app.get(
62 49 route_path('user_autocomplete_data',
63 50 params=dict(user_groups='true', active='0')),
64 51 extra_environ=xhr_header, status=200)
65 52 result = json.loads(response.body)
66 53 values = [suggestion['value'] for suggestion in result['suggestions']]
67 54 assert user_name in values
68 55
69 56 response = self.app.get(
70 57 route_path('user_autocomplete_data',
71 58 params=dict(user_groups='true', active='1')),
72 59 extra_environ=xhr_header, status=200)
73 60 result = json.loads(response.body)
74 61 values = [suggestion['value'] for suggestion in result['suggestions']]
75 62 assert user_name not in values
76 63
77 64 def test_returns_groups_when_user_groups_flag_sent(
78 65 self, user_util, xhr_header):
79 66 self.log_user()
80 67 group = user_util.create_user_group(user_groups_active=True)
81 68 group_name = group.users_group_name
82 69 response = self.app.get(
83 70 route_path('user_autocomplete_data',
84 71 params=dict(user_groups='true')),
85 72 extra_environ=xhr_header, status=200)
86 73 result = json.loads(response.body)
87 74 values = [suggestion['value'] for suggestion in result['suggestions']]
88 75 assert group_name in values
89 76
90 77 @pytest.mark.parametrize('query, count', [
91 78 ('hello1', 0),
92 79 ('dev', 2),
93 80 ])
94 81 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
95 82 query, count):
96 83 self.log_user()
97 84
98 85 user_util._test_name = 'dev-test'
99 86 user_util.create_user()
100 87
101 88 user_util._test_name = 'dev-group-test'
102 89 user_util.create_user_group()
103 90
104 91 response = self.app.get(
105 92 route_path('user_autocomplete_data',
106 93 params=dict(user_groups='true', query=query)),
107 94 extra_environ=xhr_header, status=200)
108 95
109 96 result = json.loads(response.body)
110 97 assert len(result['suggestions']) == count
@@ -1,116 +1,102 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 # Copyright (C) 2016-2023 RhodeCode GmbH
21 21 #
22 22 # This program is free software: you can redistribute it and/or modify
23 23 # it under the terms of the GNU Affero General Public License, version 3
24 24 # (only), as published by the Free Software Foundation.
25 25 #
26 26 # This program is distributed in the hope that it will be useful,
27 27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 29 # GNU General Public License for more details.
30 30 #
31 31 # You should have received a copy of the GNU Affero General Public License
32 32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 33 #
34 34 # This program is dual-licensed. If you wish to learn more about the
35 35 # RhodeCode Enterprise Edition, including its added features, Support services,
36 36 # and proprietary license terms, please see https://rhodecode.com/licenses/
37 37
38 38 import pytest
39 39
40 from rhodecode.lib.ext_json import json
41
40 42 from rhodecode.tests import TestController
41 43 from rhodecode.tests.fixture import Fixture
42 from rhodecode.lib.ext_json import json
43
44 from rhodecode.tests.routes import route_path
44 45
45 46 fixture = Fixture()
46 47
47 48
48 def route_path(name, params=None, **kwargs):
49 import urllib.request
50 import urllib.parse
51 import urllib.error
52
53 base_url = {
54 'user_autocomplete_data': '/_users',
55 'user_group_autocomplete_data': '/_user_groups'
56 }[name].format(**kwargs)
57
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 return base_url
61
62
63 49 class TestUserGroupAutocompleteData(TestController):
64 50
65 51 def test_returns_list_of_user_groups(self, user_util, xhr_header):
66 52 self.log_user()
67 53 user_group = user_util.create_user_group(active=True)
68 54 user_group_name = user_group.users_group_name
69 55 response = self.app.get(
70 56 route_path('user_group_autocomplete_data'),
71 57 extra_environ=xhr_header, status=200)
72 58 result = json.loads(response.body)
73 59 values = [suggestion['value'] for suggestion in result['suggestions']]
74 60 assert user_group_name in values
75 61
76 62 def test_returns_inactive_user_groups_when_active_flag_sent(
77 63 self, user_util, xhr_header):
78 64 self.log_user()
79 65 user_group = user_util.create_user_group(active=False)
80 66 user_group_name = user_group.users_group_name
81 67
82 68 response = self.app.get(
83 69 route_path('user_group_autocomplete_data',
84 70 params=dict(active='0')),
85 71 extra_environ=xhr_header, status=200)
86 72 result = json.loads(response.body)
87 73 values = [suggestion['value'] for suggestion in result['suggestions']]
88 74 assert user_group_name in values
89 75
90 76 response = self.app.get(
91 77 route_path('user_group_autocomplete_data',
92 78 params=dict(active='1')),
93 79 extra_environ=xhr_header, status=200)
94 80 result = json.loads(response.body)
95 81 values = [suggestion['value'] for suggestion in result['suggestions']]
96 82 assert user_group_name not in values
97 83
98 84 @pytest.mark.parametrize('query, count', [
99 85 ('hello1', 0),
100 86 ('dev', 1),
101 87 ])
102 88 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
103 89 self.log_user()
104 90
105 91 user_util._test_name = 'dev-test'
106 92 user_util.create_user_group()
107 93
108 94 response = self.app.get(
109 95 route_path('user_group_autocomplete_data',
110 96 params=dict(user_groups='true',
111 97 query=query)),
112 98 extra_environ=xhr_header, status=200)
113 99
114 100 result = json.loads(response.body)
115 101
116 102 assert len(result['suggestions']) == count
@@ -1,177 +1,167 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import pytest
21 21
22 22 import rhodecode
23 23 from rhodecode.model.db import Repository, RepoGroup, User
24 24 from rhodecode.model.meta import Session
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.repo_group import RepoGroupModel
27 25 from rhodecode.model.settings import SettingsModel
28 26 from rhodecode.tests import TestController
29 27 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib import helpers as h
31
32 fixture = Fixture()
28 from rhodecode.tests.routes import route_path
33 29
34 30
35 def route_path(name, **kwargs):
36 return {
37 'home': '/',
38 'main_page_repos_data': '/_home_repos',
39 'main_page_repo_groups_data': '/_home_repo_groups',
40 'repo_group_home': '/{repo_group_name}'
41 }[name].format(**kwargs)
31 fixture = Fixture()
42 32
43 33
44 34 class TestHomeController(TestController):
45 35
46 36 def test_index(self):
47 37 self.log_user()
48 38 response = self.app.get(route_path('home'))
49 39 # if global permission is set
50 40 response.mustcontain('New Repository')
51 41
52 42 def test_index_grid_repos(self, xhr_header):
53 43 self.log_user()
54 44 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
55 45 # search for objects inside the JavaScript JSON
56 46 for obj in Repository.getAll():
57 47 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
58 48
59 49 def test_index_grid_repo_groups(self, xhr_header):
60 50 self.log_user()
61 51 response = self.app.get(route_path('main_page_repo_groups_data'),
62 52 extra_environ=xhr_header,)
63 53
64 54 # search for objects inside the JavaScript JSON
65 55 for obj in RepoGroup.getAll():
66 56 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
67 57
68 58 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
69 59 user = user_util.create_user(password='qweqwe')
70 60 group_ok = user_util.create_repo_group(owner=user)
71 61 group_id_ok = group_ok.group_id
72 62
73 63 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
74 64 group_id_forbidden = group_forbidden.group_id
75 65
76 66 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
77 67 self.log_user(user.username, 'qweqwe')
78 68
79 69 self.app.get(route_path('main_page_repo_groups_data'),
80 70 extra_environ=xhr_header,
81 71 params={'repo_group_id': group_id_ok}, status=200)
82 72
83 73 self.app.get(route_path('main_page_repo_groups_data'),
84 74 extra_environ=xhr_header,
85 75 params={'repo_group_id': group_id_forbidden}, status=404)
86 76
87 77 def test_index_contains_statics_with_ver(self):
88 78 from rhodecode.lib.base import calculate_version_hash
89 79
90 80 self.log_user()
91 81 response = self.app.get(route_path('home'))
92 82
93 83 rhodecode_version_hash = calculate_version_hash(
94 84 {'beaker.session.secret': 'test-rc-uytcxaz'})
95 85 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
96 86 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
97 87
98 88 def test_index_contains_backend_specific_details(self, backend, xhr_header):
99 89 self.log_user()
100 90 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
101 91 tip = backend.repo.get_commit().raw_id
102 92
103 93 # html in javascript variable:
104 94 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
105 95 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
106 96
107 97 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
108 98 response.mustcontain("""Added a symlink""")
109 99
110 100 def test_index_with_anonymous_access_disabled(self):
111 101 with fixture.anon_access(False):
112 102 response = self.app.get(route_path('home'), status=302)
113 103 assert 'login' in response.location
114 104
115 105 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
116 106 group_id = 918123
117 107 self.app.get(
118 108 route_path('main_page_repo_groups_data'),
119 109 params={'repo_group_id': group_id},
120 110 status=404, extra_environ=xhr_header)
121 111
122 112 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
123 113 gr = user_util.create_repo_group()
124 114 repo = user_util.create_repo(parent=gr)
125 115 repo_name = repo.repo_name
126 116 group_id = gr.group_id
127 117
128 118 response = self.app.get(route_path(
129 119 'repo_group_home', repo_group_name=gr.group_name))
130 120 response.mustcontain('d.repo_group_id = {}'.format(group_id))
131 121
132 122 response = self.app.get(
133 123 route_path('main_page_repos_data'),
134 124 params={'repo_group_id': group_id},
135 125 extra_environ=xhr_header,)
136 126 response.mustcontain(repo_name)
137 127
138 128 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
139 129 gr = user_util.create_repo_group()
140 130 repo = user_util.create_repo(parent=gr)
141 131 repo_name = repo.repo_name
142 132 group_id = gr.group_id
143 133
144 134 response = self.app.get(route_path(
145 135 'repo_group_home', repo_group_name=gr.group_name+'/'))
146 136 response.mustcontain('d.repo_group_id = {}'.format(group_id))
147 137
148 138 response = self.app.get(
149 139 route_path('main_page_repos_data'),
150 140 params={'repo_group_id': group_id},
151 141 extra_environ=xhr_header, )
152 142 response.mustcontain(repo_name)
153 143
154 144 @pytest.mark.parametrize("name, state", [
155 145 ('Disabled', False),
156 146 ('Enabled', True),
157 147 ])
158 148 def test_index_show_version(self, autologin_user, name, state):
159 149 version_string = 'RhodeCode %s' % rhodecode.__version__
160 150
161 151 sett = SettingsModel().create_or_update_setting(
162 152 'show_version', state, 'bool')
163 153 Session().add(sett)
164 154 Session().commit()
165 155 SettingsModel().invalidate_settings_cache(hard=True)
166 156
167 157 response = self.app.get(route_path('home'))
168 158 if state is True:
169 159 response.mustcontain(version_string)
170 160 if state is False:
171 161 response.mustcontain(no=[version_string])
172 162
173 163 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
174 164 response = self.app.get(route_path('home'))
175 165 assert_response = response.assert_response()
176 166 element = assert_response.get_element('.logout [name=csrf_token]')
177 167 assert element.value == csrf_token
@@ -1,107 +1,87 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import datetime
21 21
22 22 import pytest
23 23
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.tests import TestController
26 25 from rhodecode.model.db import UserFollowing, Repository
27 26
28
29 def route_path(name, params=None, **kwargs):
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'journal': ADMIN_PREFIX + '/journal',
36 'journal_rss': ADMIN_PREFIX + '/journal/rss',
37 'journal_atom': ADMIN_PREFIX + '/journal/atom',
38 'journal_public': ADMIN_PREFIX + '/public_journal',
39 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
40 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
41 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
42 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
43 'toggle_following': ADMIN_PREFIX + '/toggle_following',
44 }[name].format(**kwargs)
45
46 if params:
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 return base_url
27 from rhodecode.tests import TestController
28 from rhodecode.tests.routes import route_path
49 29
50 30
51 31 class TestJournalViews(TestController):
52 32
53 33 def test_journal(self):
54 34 self.log_user()
55 35 response = self.app.get(route_path('journal'))
56 36 # response.mustcontain(
57 37 # """<div class="journal_day">%s</div>""" % datetime.date.today())
58 38
59 39 @pytest.mark.parametrize("feed_type, content_type", [
60 40 ('rss', "application/rss+xml"),
61 41 ('atom', "application/atom+xml")
62 42 ])
63 43 def test_journal_feed(self, feed_type, content_type):
64 44 self.log_user()
65 45 response = self.app.get(
66 46 route_path(
67 47 'journal_{}'.format(feed_type)),
68 48 status=200)
69 49
70 50 assert response.content_type == content_type
71 51
72 52 def test_toggle_following_repository(self, backend):
73 53 user = self.log_user()
74 54 repo = Repository.get_by_repo_name(backend.repo_name)
75 55 repo_id = repo.repo_id
76 56 self.app.post(
77 57 route_path('toggle_following'), {'follows_repo_id': repo_id,
78 58 'csrf_token': self.csrf_token})
79 59
80 60 followings = UserFollowing.query()\
81 61 .filter(UserFollowing.user_id == user['user_id'])\
82 62 .filter(UserFollowing.follows_repo_id == repo_id).all()
83 63
84 64 assert len(followings) == 0
85 65
86 66 self.app.post(
87 67 route_path('toggle_following'), {'follows_repo_id': repo_id,
88 68 'csrf_token': self.csrf_token})
89 69
90 70 followings = UserFollowing.query()\
91 71 .filter(UserFollowing.user_id == user['user_id'])\
92 72 .filter(UserFollowing.follows_repo_id == repo_id).all()
93 73
94 74 assert len(followings) == 1
95 75
96 76 @pytest.mark.parametrize("feed_type, content_type", [
97 77 ('rss', "application/rss+xml"),
98 78 ('atom', "application/atom+xml")
99 79 ])
100 80 def test_public_journal_feed(self, feed_type, content_type):
101 81 self.log_user()
102 82 response = self.app.get(
103 83 route_path(
104 84 'journal_public_{}'.format(feed_type)),
105 85 status=200)
106 86
107 87 assert response.content_type == content_type
@@ -1,607 +1,581 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import urllib.parse
20 20
21 21 import mock
22 22 import pytest
23 23
24 from rhodecode.tests import (
25 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
26 no_newline_id_generator)
27 from rhodecode.tests.fixture import Fixture
24
28 25 from rhodecode.lib.auth import check_password
29 26 from rhodecode.lib import helpers as h
30 27 from rhodecode.model.auth_token import AuthTokenModel
31 28 from rhodecode.model.db import User, Notification, UserApiKeys
32 29 from rhodecode.model.meta import Session
33 30
31 from rhodecode.tests import (
32 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
33 no_newline_id_generator)
34 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.routes import route_path
36
34 37 fixture = Fixture()
35 38
36 39 whitelist_view = ['RepoCommitsView:repo_commit_raw']
37 40
38 41
39 def route_path(name, params=None, **kwargs):
40 import urllib.request
41 import urllib.parse
42 import urllib.error
43 from rhodecode.apps._base import ADMIN_PREFIX
44
45 base_url = {
46 'login': ADMIN_PREFIX + '/login',
47 'logout': ADMIN_PREFIX + '/logout',
48 'register': ADMIN_PREFIX + '/register',
49 'reset_password':
50 ADMIN_PREFIX + '/password_reset',
51 'reset_password_confirmation':
52 ADMIN_PREFIX + '/password_reset_confirmation',
53
54 'admin_permissions_application':
55 ADMIN_PREFIX + '/permissions/application',
56 'admin_permissions_application_update':
57 ADMIN_PREFIX + '/permissions/application/update',
58
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60
61 }[name].format(**kwargs)
62
63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
65 return base_url
66
67
68 42 @pytest.mark.usefixtures('app')
69 43 class TestLoginController(object):
70 44 destroy_users = set()
71 45
72 46 @classmethod
73 47 def teardown_class(cls):
74 48 fixture.destroy_users(cls.destroy_users)
75 49
76 50 def teardown_method(self, method):
77 51 for n in Notification.query().all():
78 52 Session().delete(n)
79 53
80 54 Session().commit()
81 55 assert Notification.query().all() == []
82 56
83 57 def test_index(self):
84 58 response = self.app.get(route_path('login'))
85 59 assert response.status == '200 OK'
86 60 # Test response...
87 61
88 62 def test_login_admin_ok(self):
89 63 response = self.app.post(route_path('login'),
90 64 {'username': 'test_admin',
91 65 'password': 'test12'}, status=302)
92 66 response = response.follow()
93 67 session = response.get_session_from_response()
94 68 username = session['rhodecode_user'].get('username')
95 69 assert username == 'test_admin'
96 70 response.mustcontain('logout')
97 71
98 72 def test_login_regular_ok(self):
99 73 response = self.app.post(route_path('login'),
100 74 {'username': 'test_regular',
101 75 'password': 'test12'}, status=302)
102 76
103 77 response = response.follow()
104 78 session = response.get_session_from_response()
105 79 username = session['rhodecode_user'].get('username')
106 80 assert username == 'test_regular'
107 81 response.mustcontain('logout')
108 82
109 83 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 84 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 85 with fixture.auth_restriction(self.app._pyramid_registry,
112 86 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 87 response = self.app.post(route_path('login'),
114 88 {'username': 'test_regular',
115 89 'password': 'test12'})
116 90
117 91 response.mustcontain('invalid user name')
118 92 response.mustcontain('invalid password')
119 93
120 94 def test_login_regular_forbidden_when_scope_restriction(self):
121 95 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 96 with fixture.scope_restriction(self.app._pyramid_registry,
123 97 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
124 98 response = self.app.post(route_path('login'),
125 99 {'username': 'test_regular',
126 100 'password': 'test12'})
127 101
128 102 response.mustcontain('invalid user name')
129 103 response.mustcontain('invalid password')
130 104
131 105 def test_login_ok_came_from(self):
132 106 test_came_from = '/_admin/users?branch=stable'
133 107 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
134 108 response = self.app.post(
135 109 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
136 110
137 111 assert 'branch=stable' in response.location
138 112 response = response.follow()
139 113
140 114 assert response.status == '200 OK'
141 115 response.mustcontain('Users administration')
142 116
143 117 def test_redirect_to_login_with_get_args(self):
144 118 with fixture.anon_access(False):
145 119 kwargs = {'branch': 'stable'}
146 120 response = self.app.get(
147 121 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
148 122 status=302)
149 123
150 124 response_query = urllib.parse.parse_qsl(response.location)
151 125 assert 'branch=stable' in response_query[0][1]
152 126
153 127 def test_login_form_with_get_args(self):
154 128 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
155 129 response = self.app.get(_url)
156 130 assert 'branch%3Dstable' in response.form.action
157 131
158 132 @pytest.mark.parametrize("url_came_from", [
159 133 'data:text/html,<script>window.alert("xss")</script>',
160 134 'mailto:test@rhodecode.org',
161 135 'file:///etc/passwd',
162 136 'ftp://some.ftp.server',
163 137 'http://other.domain',
164 138 ], ids=no_newline_id_generator)
165 139 def test_login_bad_came_froms(self, url_came_from):
166 140 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 141 response = self.app.post(
168 142 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
169 143 assert response.status == '302 Found'
170 144 response = response.follow()
171 145 assert response.status == '200 OK'
172 146 assert response.request.path == '/'
173 147
174 148 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
175 149 @pytest.mark.parametrize("url_came_from", [
176 150 '/\r\nX-Forwarded-Host: \rhttp://example.org',
177 151 ], ids=no_newline_id_generator)
178 152 def test_login_bad_came_froms_404(self, url_came_from):
179 153 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
180 154 response = self.app.post(
181 155 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
182 156
183 157 response = response.follow()
184 158 assert response.status == '404 Not Found'
185 159
186 160 def test_login_short_password(self):
187 161 response = self.app.post(route_path('login'),
188 162 {'username': 'test_admin',
189 163 'password': 'as'})
190 164 assert response.status == '200 OK'
191 165
192 166 response.mustcontain('Enter 3 characters or more')
193 167
194 168 def test_login_wrong_non_ascii_password(self, user_regular):
195 169 response = self.app.post(
196 170 route_path('login'),
197 171 {'username': user_regular.username,
198 172 'password': 'invalid-non-asci\xe4'.encode('utf8')})
199 173
200 174 response.mustcontain('invalid user name')
201 175 response.mustcontain('invalid password')
202 176
203 177 def test_login_with_non_ascii_password(self, user_util):
204 178 password = u'valid-non-ascii\xe4'
205 179 user = user_util.create_user(password=password)
206 180 response = self.app.post(
207 181 route_path('login'),
208 182 {'username': user.username,
209 183 'password': password})
210 184 assert response.status_code == 302
211 185
212 186 def test_login_wrong_username_password(self):
213 187 response = self.app.post(route_path('login'),
214 188 {'username': 'error',
215 189 'password': 'test12'})
216 190
217 191 response.mustcontain('invalid user name')
218 192 response.mustcontain('invalid password')
219 193
220 194 def test_login_admin_ok_password_migration(self, real_crypto_backend):
221 195 from rhodecode.lib import auth
222 196
223 197 # create new user, with sha256 password
224 198 temp_user = 'test_admin_sha256'
225 199 user = fixture.create_user(temp_user)
226 200 user.password = auth._RhodeCodeCryptoSha256().hash_create(
227 201 b'test123')
228 202 Session().add(user)
229 203 Session().commit()
230 204 self.destroy_users.add(temp_user)
231 205 response = self.app.post(route_path('login'),
232 206 {'username': temp_user,
233 207 'password': 'test123'}, status=302)
234 208
235 209 response = response.follow()
236 210 session = response.get_session_from_response()
237 211 username = session['rhodecode_user'].get('username')
238 212 assert username == temp_user
239 213 response.mustcontain('logout')
240 214
241 215 # new password should be bcrypted, after log-in and transfer
242 216 user = User.get_by_username(temp_user)
243 217 assert user.password.startswith('$')
244 218
245 219 # REGISTRATIONS
246 220 def test_register(self):
247 221 response = self.app.get(route_path('register'))
248 222 response.mustcontain('Create an Account')
249 223
250 224 def test_register_err_same_username(self):
251 225 uname = 'test_admin'
252 226 response = self.app.post(
253 227 route_path('register'),
254 228 {
255 229 'username': uname,
256 230 'password': 'test12',
257 231 'password_confirmation': 'test12',
258 232 'email': 'goodmail@domain.com',
259 233 'firstname': 'test',
260 234 'lastname': 'test'
261 235 }
262 236 )
263 237
264 238 assertr = response.assert_response()
265 239 msg = 'Username "%(username)s" already exists'
266 240 msg = msg % {'username': uname}
267 241 assertr.element_contains('#username+.error-message', msg)
268 242
269 243 def test_register_err_same_email(self):
270 244 response = self.app.post(
271 245 route_path('register'),
272 246 {
273 247 'username': 'test_admin_0',
274 248 'password': 'test12',
275 249 'password_confirmation': 'test12',
276 250 'email': 'test_admin@mail.com',
277 251 'firstname': 'test',
278 252 'lastname': 'test'
279 253 }
280 254 )
281 255
282 256 assertr = response.assert_response()
283 257 msg = u'This e-mail address is already taken'
284 258 assertr.element_contains('#email+.error-message', msg)
285 259
286 260 def test_register_err_same_email_case_sensitive(self):
287 261 response = self.app.post(
288 262 route_path('register'),
289 263 {
290 264 'username': 'test_admin_1',
291 265 'password': 'test12',
292 266 'password_confirmation': 'test12',
293 267 'email': 'TesT_Admin@mail.COM',
294 268 'firstname': 'test',
295 269 'lastname': 'test'
296 270 }
297 271 )
298 272 assertr = response.assert_response()
299 273 msg = u'This e-mail address is already taken'
300 274 assertr.element_contains('#email+.error-message', msg)
301 275
302 276 def test_register_err_wrong_data(self):
303 277 response = self.app.post(
304 278 route_path('register'),
305 279 {
306 280 'username': 'xs',
307 281 'password': 'test',
308 282 'password_confirmation': 'test',
309 283 'email': 'goodmailm',
310 284 'firstname': 'test',
311 285 'lastname': 'test'
312 286 }
313 287 )
314 288 assert response.status == '200 OK'
315 289 response.mustcontain('An email address must contain a single @')
316 290 response.mustcontain('Enter a value 6 characters long or more')
317 291
318 292 def test_register_err_username(self):
319 293 response = self.app.post(
320 294 route_path('register'),
321 295 {
322 296 'username': 'error user',
323 297 'password': 'test12',
324 298 'password_confirmation': 'test12',
325 299 'email': 'goodmailm',
326 300 'firstname': 'test',
327 301 'lastname': 'test'
328 302 }
329 303 )
330 304
331 305 response.mustcontain('An email address must contain a single @')
332 306 response.mustcontain(
333 307 'Username may only contain '
334 308 'alphanumeric characters underscores, '
335 309 'periods or dashes and must begin with '
336 310 'alphanumeric character')
337 311
338 312 def test_register_err_case_sensitive(self):
339 313 usr = 'Test_Admin'
340 314 response = self.app.post(
341 315 route_path('register'),
342 316 {
343 317 'username': usr,
344 318 'password': 'test12',
345 319 'password_confirmation': 'test12',
346 320 'email': 'goodmailm',
347 321 'firstname': 'test',
348 322 'lastname': 'test'
349 323 }
350 324 )
351 325
352 326 assertr = response.assert_response()
353 327 msg = u'Username "%(username)s" already exists'
354 328 msg = msg % {'username': usr}
355 329 assertr.element_contains('#username+.error-message', msg)
356 330
357 331 def test_register_special_chars(self):
358 332 response = self.app.post(
359 333 route_path('register'),
360 334 {
361 335 'username': 'xxxaxn',
362 336 'password': 'ąćźżąśśśś',
363 337 'password_confirmation': 'ąćźżąśśśś',
364 338 'email': 'goodmailm@test.plx',
365 339 'firstname': 'test',
366 340 'lastname': 'test'
367 341 }
368 342 )
369 343
370 344 msg = u'Invalid characters (non-ascii) in password'
371 345 response.mustcontain(msg)
372 346
373 347 def test_register_password_mismatch(self):
374 348 response = self.app.post(
375 349 route_path('register'),
376 350 {
377 351 'username': 'xs',
378 352 'password': '123qwe',
379 353 'password_confirmation': 'qwe123',
380 354 'email': 'goodmailm@test.plxa',
381 355 'firstname': 'test',
382 356 'lastname': 'test'
383 357 }
384 358 )
385 359 msg = u'Passwords do not match'
386 360 response.mustcontain(msg)
387 361
388 362 def test_register_ok(self):
389 363 username = 'test_regular4'
390 364 password = 'qweqwe'
391 365 email = 'marcin@test.com'
392 366 name = 'testname'
393 367 lastname = 'testlastname'
394 368
395 369 # this initializes a session
396 370 response = self.app.get(route_path('register'))
397 371 response.mustcontain('Create an Account')
398 372
399 373
400 374 response = self.app.post(
401 375 route_path('register'),
402 376 {
403 377 'username': username,
404 378 'password': password,
405 379 'password_confirmation': password,
406 380 'email': email,
407 381 'firstname': name,
408 382 'lastname': lastname,
409 383 'admin': True
410 384 },
411 385 status=302
412 386 ) # This should be overridden
413 387
414 388 assert_session_flash(
415 389 response, 'You have successfully registered with RhodeCode. You can log-in now.')
416 390
417 391 ret = Session().query(User).filter(
418 392 User.username == 'test_regular4').one()
419 393 assert ret.username == username
420 394 assert check_password(password, ret.password)
421 395 assert ret.email == email
422 396 assert ret.name == name
423 397 assert ret.lastname == lastname
424 398 assert ret.auth_tokens is not None
425 399 assert not ret.admin
426 400
427 401 def test_forgot_password_wrong_mail(self):
428 402 bad_email = 'marcin@wrongmail.org'
429 403 # this initializes a session
430 404 self.app.get(route_path('reset_password'))
431 405
432 406 response = self.app.post(
433 407 route_path('reset_password'), {'email': bad_email, }
434 408 )
435 409 assert_session_flash(response,
436 410 'If such email exists, a password reset link was sent to it.')
437 411
438 412 def test_forgot_password(self, user_util):
439 413 # this initializes a session
440 414 self.app.get(route_path('reset_password'))
441 415
442 416 user = user_util.create_user()
443 417 user_id = user.user_id
444 418 email = user.email
445 419
446 420 response = self.app.post(route_path('reset_password'), {'email': email, })
447 421
448 422 assert_session_flash(response,
449 423 'If such email exists, a password reset link was sent to it.')
450 424
451 425 # BAD KEY
452 426 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
453 427 response = self.app.get(confirm_url, status=302)
454 428 assert response.location.endswith(route_path('reset_password'))
455 429 assert_session_flash(response, 'Given reset token is invalid')
456 430
457 431 response.follow() # cleanup flash
458 432
459 433 # GOOD KEY
460 434 key = UserApiKeys.query()\
461 435 .filter(UserApiKeys.user_id == user_id)\
462 436 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
463 437 .first()
464 438
465 439 assert key
466 440
467 441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
468 442 response = self.app.get(confirm_url)
469 443 assert response.status == '302 Found'
470 444 assert response.location.endswith(route_path('login'))
471 445
472 446 assert_session_flash(
473 447 response,
474 448 'Your password reset was successful, '
475 449 'a new password has been sent to your email')
476 450
477 451 response.follow()
478 452
479 453 def _get_api_whitelist(self, values=None):
480 454 config = {'api_access_controllers_whitelist': values or []}
481 455 return config
482 456
483 457 @pytest.mark.parametrize("test_name, auth_token", [
484 458 ('none', None),
485 459 ('empty_string', ''),
486 460 ('fake_number', '123456'),
487 461 ('proper_auth_token', None)
488 462 ])
489 463 def test_access_not_whitelisted_page_via_auth_token(
490 464 self, test_name, auth_token, user_admin):
491 465
492 466 whitelist = self._get_api_whitelist([])
493 467 with mock.patch.dict('rhodecode.CONFIG', whitelist):
494 468 assert [] == whitelist['api_access_controllers_whitelist']
495 469 if test_name == 'proper_auth_token':
496 470 # use builtin if api_key is None
497 471 auth_token = user_admin.api_key
498 472
499 473 with fixture.anon_access(False):
500 474 # webtest uses linter to check if response is bytes,
501 475 # and we use memoryview here as a wrapper, quick turn-off
502 476 self.app.lint = False
503 477
504 478 self.app.get(
505 479 route_path('repo_commit_raw',
506 480 repo_name=HG_REPO, commit_id='tip',
507 481 params=dict(api_key=auth_token)),
508 482 status=302)
509 483
510 484 @pytest.mark.parametrize("test_name, auth_token, code", [
511 485 ('none', None, 302),
512 486 ('empty_string', '', 302),
513 487 ('fake_number', '123456', 302),
514 488 ('proper_auth_token', None, 200)
515 489 ])
516 490 def test_access_whitelisted_page_via_auth_token(
517 491 self, test_name, auth_token, code, user_admin):
518 492
519 493 whitelist = self._get_api_whitelist(whitelist_view)
520 494
521 495 with mock.patch.dict('rhodecode.CONFIG', whitelist):
522 496 assert whitelist_view == whitelist['api_access_controllers_whitelist']
523 497
524 498 if test_name == 'proper_auth_token':
525 499 auth_token = user_admin.api_key
526 500 assert auth_token
527 501
528 502 with fixture.anon_access(False):
529 503 # webtest uses linter to check if response is bytes,
530 504 # and we use memoryview here as a wrapper, quick turn-off
531 505 self.app.lint = False
532 506 self.app.get(
533 507 route_path('repo_commit_raw',
534 508 repo_name=HG_REPO, commit_id='tip',
535 509 params=dict(api_key=auth_token)),
536 510 status=code)
537 511
538 512 @pytest.mark.parametrize("test_name, auth_token, code", [
539 513 ('proper_auth_token', None, 200),
540 514 ('wrong_auth_token', '123456', 302),
541 515 ])
542 516 def test_access_whitelisted_page_via_auth_token_bound_to_token(
543 517 self, test_name, auth_token, code, user_admin):
544 518
545 519 expected_token = auth_token
546 520 if test_name == 'proper_auth_token':
547 521 auth_token = user_admin.api_key
548 522 expected_token = auth_token
549 523 assert auth_token
550 524
551 525 whitelist = self._get_api_whitelist([
552 526 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
553 527
554 528 with mock.patch.dict('rhodecode.CONFIG', whitelist):
555 529
556 530 with fixture.anon_access(False):
557 531 # webtest uses linter to check if response is bytes,
558 532 # and we use memoryview here as a wrapper, quick turn-off
559 533 self.app.lint = False
560 534
561 535 self.app.get(
562 536 route_path('repo_commit_raw',
563 537 repo_name=HG_REPO, commit_id='tip',
564 538 params=dict(api_key=auth_token)),
565 539 status=code)
566 540
567 541 def test_access_page_via_extra_auth_token(self):
568 542 whitelist = self._get_api_whitelist(whitelist_view)
569 543 with mock.patch.dict('rhodecode.CONFIG', whitelist):
570 544 assert whitelist_view == \
571 545 whitelist['api_access_controllers_whitelist']
572 546
573 547 new_auth_token = AuthTokenModel().create(
574 548 TEST_USER_ADMIN_LOGIN, 'test')
575 549 Session().commit()
576 550 with fixture.anon_access(False):
577 551 # webtest uses linter to check if response is bytes,
578 552 # and we use memoryview here as a wrapper, quick turn-off
579 553 self.app.lint = False
580 554 self.app.get(
581 555 route_path('repo_commit_raw',
582 556 repo_name=HG_REPO, commit_id='tip',
583 557 params=dict(api_key=new_auth_token.api_key)),
584 558 status=200)
585 559
586 560 def test_access_page_via_expired_auth_token(self):
587 561 whitelist = self._get_api_whitelist(whitelist_view)
588 562 with mock.patch.dict('rhodecode.CONFIG', whitelist):
589 563 assert whitelist_view == \
590 564 whitelist['api_access_controllers_whitelist']
591 565
592 566 new_auth_token = AuthTokenModel().create(
593 567 TEST_USER_ADMIN_LOGIN, 'test')
594 568 Session().commit()
595 569 # patch the api key and make it expired
596 570 new_auth_token.expires = 0
597 571 Session().add(new_auth_token)
598 572 Session().commit()
599 573 with fixture.anon_access(False):
600 574 # webtest uses linter to check if response is bytes,
601 575 # and we use memoryview here as a wrapper, quick turn-off
602 576 self.app.lint = False
603 577 self.app.get(
604 578 route_path('repo_commit_raw',
605 579 repo_name=HG_REPO, commit_id='tip',
606 580 params=dict(api_key=new_auth_token.api_key)),
607 581 status=302)
@@ -1,118 +1,94 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.lib import helpers as h
22 22 from rhodecode.tests import (
23 23 TestController, clear_cache_regions,
24 24 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.tests.utils import AssertResponse
27
28 fixture = Fixture()
27 from rhodecode.tests.routes import route_path
29 28
30 29
31 def route_path(name, params=None, **kwargs):
32 import urllib.request
33 import urllib.parse
34 import urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
36
37 base_url = {
38 'login': ADMIN_PREFIX + '/login',
39 'logout': ADMIN_PREFIX + '/logout',
40 'register': ADMIN_PREFIX + '/register',
41 'reset_password':
42 ADMIN_PREFIX + '/password_reset',
43 'reset_password_confirmation':
44 ADMIN_PREFIX + '/password_reset_confirmation',
45
46 'admin_permissions_application':
47 ADMIN_PREFIX + '/permissions/application',
48 'admin_permissions_application_update':
49 ADMIN_PREFIX + '/permissions/application/update',
50 }[name].format(**kwargs)
51
52 if params:
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 return base_url
30 fixture = Fixture()
55 31
56 32
57 33 class TestPasswordReset(TestController):
58 34
59 35 @pytest.mark.parametrize(
60 36 'pwd_reset_setting, show_link, show_reset', [
61 37 ('hg.password_reset.enabled', True, True),
62 38 ('hg.password_reset.hidden', False, True),
63 39 ('hg.password_reset.disabled', False, False),
64 40 ])
65 41 def test_password_reset_settings(
66 42 self, pwd_reset_setting, show_link, show_reset):
67 43 clear_cache_regions()
68 44 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 45 params = {
70 46 'csrf_token': self.csrf_token,
71 47 'anonymous': 'True',
72 48 'default_register': 'hg.register.auto_activate',
73 49 'default_register_message': '',
74 50 'default_password_reset': pwd_reset_setting,
75 51 'default_extern_activate': 'hg.extern_activate.auto',
76 52 }
77 53 resp = self.app.post(
78 54 route_path('admin_permissions_application_update'), params=params)
79 55 self.logout_user()
80 56
81 57 login_page = self.app.get(route_path('login'))
82 58 asr_login = AssertResponse(login_page)
83 59
84 60 if show_link:
85 61 asr_login.one_element_exists('a.pwd_reset')
86 62 else:
87 63 asr_login.no_element_exists('a.pwd_reset')
88 64
89 65 response = self.app.get(route_path('reset_password'))
90 66
91 67 assert_response = response.assert_response()
92 68 if show_reset:
93 69 response.mustcontain('Send password reset email')
94 70 assert_response.one_element_exists('#email')
95 71 assert_response.one_element_exists('#send')
96 72 else:
97 73 response.mustcontain('Password reset is disabled.')
98 74 assert_response.no_element_exists('#email')
99 75 assert_response.no_element_exists('#send')
100 76
101 77 def test_password_form_disabled(self):
102 78 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
103 79 params = {
104 80 'csrf_token': self.csrf_token,
105 81 'anonymous': 'True',
106 82 'default_register': 'hg.register.auto_activate',
107 83 'default_register_message': '',
108 84 'default_password_reset': 'hg.password_reset.disabled',
109 85 'default_extern_activate': 'hg.extern_activate.auto',
110 86 }
111 87 self.app.post(route_path('admin_permissions_application_update'), params=params)
112 88 self.logout_user()
113 89
114 90 response = self.app.post(
115 91 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
116 92 )
117 93 response = response.follow()
118 94 response.mustcontain('Password reset is disabled.')
@@ -1,109 +1,98 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22 from rhodecode.model.db import User
23 23 from rhodecode.tests import (
24 TestController, route_path_generator, assert_session_flash)
24 TestController, assert_session_flash)
25 25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.utils import AssertResponse
27
28 fixture = Fixture()
26 from rhodecode.tests.routes import route_path
29 27
30 28
31 def route_path(name, params=None, **kwargs):
32 url_defs = {
33 'my_account_auth_tokens':
34 ADMIN_PREFIX + '/my_account/auth_tokens',
35 'my_account_auth_tokens_add':
36 ADMIN_PREFIX + '/my_account/auth_tokens/new',
37 'my_account_auth_tokens_delete':
38 ADMIN_PREFIX + '/my_account/auth_tokens/delete',
39 }
40 return route_path_generator(url_defs, name=name, params=params, **kwargs)
29 fixture = Fixture()
41 30
42 31
43 32 class TestMyAccountAuthTokens(TestController):
44 33
45 34 def test_my_account_auth_tokens(self):
46 35 usr = self.log_user('test_regular2', 'test12')
47 36 user = User.get(usr['user_id'])
48 37 response = self.app.get(route_path('my_account_auth_tokens'))
49 38 for token in user.auth_tokens:
50 39 response.mustcontain(token[:4])
51 40 response.mustcontain('never')
52 41
53 42 def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
54 43 user = user_util.create_user(password='qweqwe')
55 44 self.log_user(user.username, 'qweqwe')
56 45
57 46 self.app.post(
58 47 route_path('my_account_auth_tokens_add'),
59 48 {'description': 'desc', 'lifetime': -1}, status=403)
60 49
61 50 @pytest.mark.parametrize("desc, lifetime", [
62 51 ('forever', -1),
63 52 ('5mins', 60*5),
64 53 ('30days', 60*60*24*30),
65 54 ])
66 55 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
67 56 user = user_util.create_user(password='qweqwe')
68 57 user_id = user.user_id
69 58 self.log_user(user.username, 'qweqwe')
70 59
71 60 response = self.app.post(
72 61 route_path('my_account_auth_tokens_add'),
73 62 {'description': desc, 'lifetime': lifetime,
74 63 'csrf_token': self.csrf_token})
75 64 assert_session_flash(response, 'Auth token successfully created')
76 65
77 66 response = response.follow()
78 67 user = User.get(user_id)
79 68 for auth_token in user.auth_tokens:
80 69 response.mustcontain(auth_token[:4])
81 70
82 71 def test_my_account_delete_auth_token(self, user_util):
83 72 user = user_util.create_user(password='qweqwe')
84 73 user_id = user.user_id
85 74 self.log_user(user.username, 'qweqwe')
86 75
87 76 user = User.get(user_id)
88 77 keys = user.get_auth_tokens()
89 78 assert 2 == len(keys)
90 79
91 80 response = self.app.post(
92 81 route_path('my_account_auth_tokens_add'),
93 82 {'description': 'desc', 'lifetime': -1,
94 83 'csrf_token': self.csrf_token})
95 84 assert_session_flash(response, 'Auth token successfully created')
96 85 response.follow()
97 86
98 87 user = User.get(user_id)
99 88 keys = user.get_auth_tokens()
100 89 assert 3 == len(keys)
101 90
102 91 response = self.app.post(
103 92 route_path('my_account_auth_tokens_delete'),
104 93 {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token})
105 94 assert_session_flash(response, 'Auth token successfully deleted')
106 95
107 96 user = User.get(user_id)
108 97 keys = user.auth_tokens
109 98 assert 2 == len(keys)
@@ -1,208 +1,190 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 # Copyright (C) 2016-2023 RhodeCode GmbH
21 21 #
22 22 # This program is free software: you can redistribute it and/or modify
23 23 # it under the terms of the GNU Affero General Public License, version 3
24 24 # (only), as published by the Free Software Foundation.
25 25 #
26 26 # This program is distributed in the hope that it will be useful,
27 27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 29 # GNU General Public License for more details.
30 30 #
31 31 # You should have received a copy of the GNU Affero General Public License
32 32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 33 #
34 34 # This program is dual-licensed. If you wish to learn more about the
35 35 # RhodeCode Enterprise Edition, including its added features, Support services,
36 36 # and proprietary license terms, please see https://rhodecode.com/licenses/
37 37
38 38 import pytest
39 39
40 40 from rhodecode.model.db import User
41 41 from rhodecode.tests import TestController, assert_session_flash
42 from rhodecode.lib import helpers as h
43
44
45 def route_path(name, params=None, **kwargs):
46 import urllib.request
47 import urllib.parse
48 import urllib.error
49 from rhodecode.apps._base import ADMIN_PREFIX
50
51 base_url = {
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 }[name].format(**kwargs)
57
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 return base_url
42 from rhodecode.tests.routes import route_path
61 43
62 44
63 45 class TestMyAccountEdit(TestController):
64 46
65 47 def test_my_account_edit(self):
66 48 self.log_user()
67 49 response = self.app.get(route_path('my_account_edit'))
68 50
69 51 response.mustcontain('value="test_admin')
70 52
71 53 @pytest.mark.backends("git", "hg")
72 54 def test_my_account_my_pullrequests(self, pr_util):
73 55 self.log_user()
74 56 response = self.app.get(route_path('my_account_pullrequests'))
75 57 response.mustcontain('There are currently no open pull '
76 58 'requests requiring your participation.')
77 59
78 60 @pytest.mark.backends("git", "hg")
79 61 @pytest.mark.parametrize('params, expected_title', [
80 62 ({'closed': 1}, 'Closed'),
81 63 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 64 ])
83 65 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
84 66 self.log_user()
85 67 response = self.app.get(route_path('my_account_pullrequests_data'),
86 68 extra_environ=xhr_header)
87 69 assert response.json == {
88 70 'data': [], 'draw': None,
89 71 'recordsFiltered': 0, 'recordsTotal': 0}
90 72
91 73 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 74 expected = {
93 75 'author_raw': 'RhodeCode Admin',
94 76 'name_raw': pr.pull_request_id
95 77 }
96 78 response = self.app.get(route_path('my_account_pullrequests_data'),
97 79 extra_environ=xhr_header)
98 80 assert response.json['recordsTotal'] == 1
99 81 assert response.json['data'][0]['author_raw'] == expected['author_raw']
100 82
101 83 assert response.json['data'][0]['author_raw'] == expected['author_raw']
102 84 assert response.json['data'][0]['name_raw'] == expected['name_raw']
103 85
104 86 @pytest.mark.parametrize(
105 87 "name, attrs", [
106 88 ('firstname', {'firstname': 'new_username'}),
107 89 ('lastname', {'lastname': 'new_username'}),
108 90 ('admin', {'admin': True}),
109 91 ('admin', {'admin': False}),
110 92 ('extern_type', {'extern_type': 'ldap'}),
111 93 ('extern_type', {'extern_type': None}),
112 94 # ('extern_name', {'extern_name': 'test'}),
113 95 # ('extern_name', {'extern_name': None}),
114 96 ('active', {'active': False}),
115 97 ('active', {'active': True}),
116 98 ('email', {'email': 'some@email.com'}),
117 99 ])
118 100 def test_my_account_update(self, name, attrs, user_util):
119 101 usr = user_util.create_user(password='qweqwe')
120 102 params = usr.get_api_data() # current user data
121 103 user_id = usr.user_id
122 104 self.log_user(
123 105 username=usr.username, password='qweqwe')
124 106
125 107 params.update({'password_confirmation': ''})
126 108 params.update({'new_password': ''})
127 109 params.update({'extern_type': 'rhodecode'})
128 110 params.update({'extern_name': 'rhodecode'})
129 111 params.update({'csrf_token': self.csrf_token})
130 112
131 113 params.update(attrs)
132 114 # my account page cannot set language param yet, only for admins
133 115 del params['language']
134 116 if name == 'email':
135 117 uem = user_util.create_additional_user_email(usr, attrs['email'])
136 118 email_before = User.get(user_id).email
137 119
138 120 response = self.app.post(route_path('my_account_update'), params)
139 121
140 122 assert_session_flash(
141 123 response, 'Your account was updated successfully')
142 124
143 125 del params['csrf_token']
144 126
145 127 updated_user = User.get(user_id)
146 128 updated_params = updated_user.get_api_data()
147 129 updated_params.update({'password_confirmation': ''})
148 130 updated_params.update({'new_password': ''})
149 131
150 132 params['last_login'] = updated_params['last_login']
151 133 params['last_activity'] = updated_params['last_activity']
152 134 # my account page cannot set language param yet, only for admins
153 135 # but we get this info from API anyway
154 136 params['language'] = updated_params['language']
155 137
156 138 if name == 'email':
157 139 params['emails'] = [attrs['email'], email_before]
158 140 if name == 'extern_type':
159 141 # cannot update this via form, expected value is original one
160 142 params['extern_type'] = "rhodecode"
161 143 if name == 'extern_name':
162 144 # cannot update this via form, expected value is original one
163 145 params['extern_name'] = str(user_id)
164 146 if name == 'active':
165 147 # my account cannot deactivate account
166 148 params['active'] = True
167 149 if name == 'admin':
168 150 # my account cannot make you an admin !
169 151 params['admin'] = False
170 152
171 153 assert params == updated_params
172 154
173 155 def test_my_account_update_err_email_not_exists_in_emails(self):
174 156 self.log_user()
175 157
176 158 new_email = 'test_regular@mail.com' # not in emails
177 159 params = {
178 160 'username': 'test_admin',
179 161 'new_password': 'test12',
180 162 'password_confirmation': 'test122',
181 163 'firstname': 'NewName',
182 164 'lastname': 'NewLastname',
183 165 'email': new_email,
184 166 'csrf_token': self.csrf_token,
185 167 }
186 168
187 169 response = self.app.post(route_path('my_account_update'),
188 170 params=params)
189 171
190 172 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
191 173
192 174 def test_my_account_update_bad_email_address(self):
193 175 self.log_user('test_regular2', 'test12')
194 176
195 177 new_email = 'newmail.pl'
196 178 params = {
197 179 'username': 'test_admin',
198 180 'new_password': 'test12',
199 181 'password_confirmation': 'test122',
200 182 'firstname': 'NewName',
201 183 'lastname': 'NewLastname',
202 184 'email': new_email,
203 185 'csrf_token': self.csrf_token,
204 186 }
205 187 response = self.app.post(route_path('my_account_update'),
206 188 params=params)
207 189
208 190 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,75 +1,66 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22 from rhodecode.model.db import User, UserEmailMap
23 23 from rhodecode.tests import (
24 24 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
25 25 assert_session_flash, TEST_USER_REGULAR_PASS)
26 26 from rhodecode.tests.fixture import Fixture
27
28 fixture = Fixture()
27 from rhodecode.tests.routes import route_path
29 28
30 29
31 def route_path(name, **kwargs):
32 return {
33 'my_account_emails':
34 ADMIN_PREFIX + '/my_account/emails',
35 'my_account_emails_add':
36 ADMIN_PREFIX + '/my_account/emails/new',
37 'my_account_emails_delete':
38 ADMIN_PREFIX + '/my_account/emails/delete',
39 }[name].format(**kwargs)
30 fixture = Fixture()
40 31
41 32
42 33 class TestMyAccountEmails(TestController):
43 34 def test_my_account_my_emails(self):
44 35 self.log_user()
45 36 response = self.app.get(route_path('my_account_emails'))
46 37 response.mustcontain('No additional emails specified')
47 38
48 39 def test_my_account_my_emails_add_remove(self):
49 40 self.log_user()
50 41 response = self.app.get(route_path('my_account_emails'))
51 42 response.mustcontain('No additional emails specified')
52 43
53 44 response = self.app.post(route_path('my_account_emails_add'),
54 45 {'email': 'foo@barz.com',
55 46 'current_password': TEST_USER_REGULAR_PASS,
56 47 'csrf_token': self.csrf_token})
57 48
58 49 response = self.app.get(route_path('my_account_emails'))
59 50
60 51 email_id = UserEmailMap.query().filter(
61 52 UserEmailMap.user == User.get_by_username(
62 53 TEST_USER_ADMIN_LOGIN)).filter(
63 54 UserEmailMap.email == 'foo@barz.com').one().email_id
64 55
65 56 response.mustcontain('foo@barz.com')
66 57 response.mustcontain('<input id="del_email_id" name="del_email_id" '
67 58 'type="hidden" value="%s" />' % email_id)
68 59
69 60 response = self.app.post(
70 61 route_path('my_account_emails_delete'), {
71 62 'del_email_id': email_id,
72 63 'csrf_token': self.csrf_token})
73 64 assert_session_flash(response, 'Email successfully deleted')
74 65 response = self.app.get(route_path('my_account_emails'))
75 66 response.mustcontain('No additional emails specified')
@@ -1,207 +1,186 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 from rhodecode.apps._base import ADMIN_PREFIX
22 21 from rhodecode.tests import (
23 22 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
24 23 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
26 26
27 27 from rhodecode.model.db import Notification, User
28 from rhodecode.model.user import UserModel
29 28 from rhodecode.model.notification import NotificationModel
30 29 from rhodecode.model.meta import Session
31 30
32 31 fixture = Fixture()
33 32
34 33
35 def route_path(name, params=None, **kwargs):
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
40
41 base_url = {
42 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
44 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47
48 }[name].format(**kwargs)
49
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 return base_url
53
54
55 34 class TestNotificationsController(TestController):
56 35
57 36 def teardown_method(self, method):
58 37 for n in Notification.query().all():
59 38 inst = Notification.get(n.notification_id)
60 39 Session().delete(inst)
61 40 Session().commit()
62 41
63 42 def test_mark_all_read(self, user_util):
64 43 user = user_util.create_user(password='qweqwe')
65 44 self.log_user(user.username, 'qweqwe')
66 45
67 46 self.app.post(
68 47 route_path('notifications_mark_all_read'), status=302,
69 48 params={'csrf_token': self.csrf_token}
70 49 )
71 50
72 51 def test_show_all(self, user_util):
73 52 user = user_util.create_user(password='qweqwe')
74 53 user_id = user.user_id
75 54 self.log_user(user.username, 'qweqwe')
76 55
77 56 response = self.app.get(
78 57 route_path('notifications_show_all', params={'type': 'all'}))
79 58 response.mustcontain(
80 59 '<div class="table">No notifications here yet</div>')
81 60
82 61 notification = NotificationModel().create(
83 62 created_by=user_id, notification_subject=u'test_notification_1',
84 63 notification_body=u'notification_1', recipients=[user_id])
85 64 Session().commit()
86 65 notification_id = notification.notification_id
87 66
88 67 response = self.app.get(route_path('notifications_show_all',
89 68 params={'type': 'all'}))
90 69 response.mustcontain('id="notification_%s"' % notification_id)
91 70
92 71 def test_show_unread(self, user_util):
93 72 user = user_util.create_user(password='qweqwe')
94 73 user_id = user.user_id
95 74 self.log_user(user.username, 'qweqwe')
96 75
97 76 response = self.app.get(route_path('notifications_show_all'))
98 77 response.mustcontain(
99 78 '<div class="table">No notifications here yet</div>')
100 79
101 80 notification = NotificationModel().create(
102 81 created_by=user_id, notification_subject=u'test_notification_1',
103 82 notification_body=u'notification_1', recipients=[user_id])
104 83
105 84 # mark the USER notification as unread
106 85 user_notification = NotificationModel().get_user_notification(
107 86 user_id, notification)
108 87 user_notification.read = False
109 88
110 89 Session().commit()
111 90 notification_id = notification.notification_id
112 91
113 92 response = self.app.get(route_path('notifications_show_all'))
114 93 response.mustcontain('id="notification_%s"' % notification_id)
115 94 response.mustcontain('<div class="desc unread')
116 95
117 96 @pytest.mark.parametrize('user,password', [
118 97 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
119 98 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
120 99 ])
121 100 def test_delete(self, user, password, user_util):
122 101 self.log_user(user, password)
123 102 cur_user = self._get_logged_user()
124 103
125 104 u1 = user_util.create_user()
126 105 u2 = user_util.create_user()
127 106
128 107 # make notifications
129 108 notification = NotificationModel().create(
130 109 created_by=cur_user, notification_subject=u'test',
131 110 notification_body=u'hi there', recipients=[cur_user, u1, u2])
132 111 Session().commit()
133 112 u1 = User.get(u1.user_id)
134 113 u2 = User.get(u2.user_id)
135 114
136 115 # check DB
137 116 def get_notif(un):
138 117 return [x.notification for x in un]
139 118 assert get_notif(cur_user.notifications) == [notification]
140 119 assert get_notif(u1.notifications) == [notification]
141 120 assert get_notif(u2.notifications) == [notification]
142 121 cur_usr_id = cur_user.user_id
143 122
144 123 response = self.app.post(
145 124 route_path('notifications_delete',
146 125 notification_id=notification.notification_id),
147 126 params={'csrf_token': self.csrf_token})
148 127 assert response.json == 'ok'
149 128
150 129 cur_user = User.get(cur_usr_id)
151 130 assert cur_user.notifications == []
152 131
153 132 @pytest.mark.parametrize('user,password', [
154 133 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
155 134 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
156 135 ])
157 136 def test_show(self, user, password, user_util):
158 137 self.log_user(user, password)
159 138 cur_user = self._get_logged_user()
160 139 u1 = user_util.create_user()
161 140 u2 = user_util.create_user()
162 141
163 142 subject = u'test'
164 143 notif_body = u'hi there'
165 144 notification = NotificationModel().create(
166 145 created_by=cur_user, notification_subject=subject,
167 146 notification_body=notif_body, recipients=[cur_user, u1, u2])
168 147 Session().commit()
169 148
170 149 response = self.app.get(
171 150 route_path('notifications_show',
172 151 notification_id=notification.notification_id))
173 152
174 153 response.mustcontain(subject)
175 154 response.mustcontain(notif_body)
176 155
177 156 @pytest.mark.parametrize('user,password', [
178 157 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
179 158 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
180 159 ])
181 160 def test_update(self, user, password, user_util):
182 161 self.log_user(user, password)
183 162 cur_user = self._get_logged_user()
184 163 u1 = user_util.create_user()
185 164 u2 = user_util.create_user()
186 165
187 166 # make notifications
188 167 recipients = [cur_user, u1, u2]
189 168 notification = NotificationModel().create(
190 169 created_by=cur_user, notification_subject=u'test',
191 170 notification_body=u'hi there', recipients=recipients)
192 171 Session().commit()
193 172
194 173 for u_obj in recipients:
195 174 # if it's current user, he has his message already read
196 175 read = u_obj.username == user
197 176 assert len(u_obj.notifications) == 1
198 177 assert u_obj.notifications[0].read == read
199 178
200 179 response = self.app.post(
201 180 route_path('notifications_update',
202 181 notification_id=notification.notification_id),
203 182 params={'csrf_token': self.csrf_token})
204 183 assert response.json == 'ok'
205 184
206 185 cur_user = self._get_logged_user()
207 186 assert True is cur_user.notifications[0].read
@@ -1,143 +1,134 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21 import mock
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.lib.auth import check_password
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.user import UserModel
28 28 from rhodecode.tests import assert_session_flash
29 29 from rhodecode.tests.fixture import Fixture, TestController, error_function
30 from rhodecode.tests.routes import route_path
30 31
31 32 fixture = Fixture()
32 33
33 34
34 def route_path(name, **kwargs):
35 return {
36 'home': '/',
37 'my_account_password':
38 ADMIN_PREFIX + '/my_account/password',
39 'my_account_password_update':
40 ADMIN_PREFIX + '/my_account/password/update',
41 }[name].format(**kwargs)
42
43
44 35 test_user_1 = 'testme'
45 36 test_user_1_password = '0jd83nHNS/d23n'
46 37
47 38
48 39 class TestMyAccountPassword(TestController):
49 40 def test_valid_change_password(self, user_util):
50 41 new_password = 'my_new_valid_password'
51 42 user = user_util.create_user(password=test_user_1_password)
52 43 self.log_user(user.username, test_user_1_password)
53 44
54 45 form_data = [
55 46 ('current_password', test_user_1_password),
56 47 ('__start__', 'new_password:mapping'),
57 48 ('new_password', new_password),
58 49 ('new_password-confirm', new_password),
59 50 ('__end__', 'new_password:mapping'),
60 51 ('csrf_token', self.csrf_token),
61 52 ]
62 53 response = self.app.post(
63 54 route_path('my_account_password_update'), form_data).follow()
64 55 assert 'Successfully updated password' in response
65 56
66 57 # check_password depends on user being in session
67 58 Session().add(user)
68 59 try:
69 60 assert check_password(new_password, user.password)
70 61 finally:
71 62 Session().expunge(user)
72 63
73 64 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
74 65 ('', 'abcdef123', 'abcdef123'),
75 66 ('wrong_pw', 'abcdef123', 'abcdef123'),
76 67 (test_user_1_password, test_user_1_password, test_user_1_password),
77 68 (test_user_1_password, '', ''),
78 69 (test_user_1_password, 'abcdef123', ''),
79 70 (test_user_1_password, '', 'abcdef123'),
80 71 (test_user_1_password, 'not_the', 'same_pw'),
81 72 (test_user_1_password, 'short', 'short'),
82 73 ])
83 74 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
84 75 user_util):
85 76 user = user_util.create_user(password=test_user_1_password)
86 77 self.log_user(user.username, test_user_1_password)
87 78
88 79 form_data = [
89 80 ('current_password', current_pw),
90 81 ('__start__', 'new_password:mapping'),
91 82 ('new_password', new_pw),
92 83 ('new_password-confirm', confirm_pw),
93 84 ('__end__', 'new_password:mapping'),
94 85 ('csrf_token', self.csrf_token),
95 86 ]
96 87 response = self.app.post(
97 88 route_path('my_account_password_update'), form_data)
98 89
99 90 assert_response = response.assert_response()
100 91 assert assert_response.get_elements('.error-block')
101 92
102 93 @mock.patch.object(UserModel, 'update_user', error_function)
103 94 def test_invalid_change_password_exception(self, user_util):
104 95 user = user_util.create_user(password=test_user_1_password)
105 96 self.log_user(user.username, test_user_1_password)
106 97
107 98 form_data = [
108 99 ('current_password', test_user_1_password),
109 100 ('__start__', 'new_password:mapping'),
110 101 ('new_password', '123456'),
111 102 ('new_password-confirm', '123456'),
112 103 ('__end__', 'new_password:mapping'),
113 104 ('csrf_token', self.csrf_token),
114 105 ]
115 106 response = self.app.post(
116 107 route_path('my_account_password_update'), form_data)
117 108 assert_session_flash(
118 109 response, 'Error occurred during update of user password')
119 110
120 111 def test_password_is_updated_in_session_on_password_change(self, user_util):
121 112 old_password = 'abcdef123'
122 113 new_password = 'abcdef124'
123 114
124 115 user = user_util.create_user(password=old_password)
125 116 session = self.log_user(user.username, old_password)
126 117 old_password_hash = session['password']
127 118
128 119 form_data = [
129 120 ('current_password', old_password),
130 121 ('__start__', 'new_password:mapping'),
131 122 ('new_password', new_password),
132 123 ('new_password-confirm', new_password),
133 124 ('__end__', 'new_password:mapping'),
134 125 ('csrf_token', self.csrf_token),
135 126 ]
136 127 self.app.post(
137 128 route_path('my_account_password_update'), form_data)
138 129
139 130 response = self.app.get(route_path('home'))
140 131 session = response.get_session_from_response()
141 132 new_password_hash = session['rhodecode_user']['password']
142 133
143 134 assert old_password_hash != new_password_hash No newline at end of file
@@ -1,54 +1,45 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 import pytest
21
22 from rhodecode.apps._base import ADMIN_PREFIX
23 20 from rhodecode.tests import (
24 21 TestController, TEST_USER_ADMIN_LOGIN,
25 22 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
26 23 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.routes import route_path
27 25
28 26 fixture = Fixture()
29 27
30 28
31 def route_path(name, **kwargs):
32 return {
33 'my_account':
34 ADMIN_PREFIX + '/my_account/profile',
35 }[name].format(**kwargs)
36
37
38 29 class TestMyAccountProfile(TestController):
39 30
40 31 def test_my_account(self):
41 32 self.log_user()
42 33 response = self.app.get(route_path('my_account'))
43 34
44 35 response.mustcontain(TEST_USER_ADMIN_LOGIN)
45 36 response.mustcontain('href="/_admin/my_account/edit"')
46 37 response.mustcontain('Photo')
47 38
48 39 def test_my_account_regular_user(self):
49 40 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
50 41 response = self.app.get(route_path('my_account'))
51 42
52 43 response.mustcontain(TEST_USER_REGULAR_LOGIN)
53 44 response.mustcontain('href="/_admin/my_account/edit"')
54 45 response.mustcontain('Photo')
@@ -1,74 +1,57 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 import pytest
21
22 from rhodecode.apps._base import ADMIN_PREFIX
23 from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing
24 from rhodecode.tests import (
25 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
26 assert_session_flash)
20 from rhodecode.model.db import User, Repository, UserFollowing
21 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
27 22 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.routes import route_path
28 24
29 25 fixture = Fixture()
30 26
31 27
32 def route_path(name, **kwargs):
33 return {
34 'my_account_repos':
35 ADMIN_PREFIX + '/my_account/repos',
36 'my_account_watched':
37 ADMIN_PREFIX + '/my_account/watched',
38 'my_account_perms':
39 ADMIN_PREFIX + '/my_account/perms',
40 'my_account_notifications':
41 ADMIN_PREFIX + '/my_account/notifications',
42 }[name].format(**kwargs)
43
44
45 28 class TestMyAccountSimpleViews(TestController):
46 29
47 30 def test_my_account_my_repos(self, autologin_user):
48 31 response = self.app.get(route_path('my_account_repos'))
49 32 repos = Repository.query().filter(
50 33 Repository.user == User.get_by_username(
51 34 TEST_USER_ADMIN_LOGIN)).all()
52 35 for repo in repos:
53 36 response.mustcontain(f'"name_raw":"{repo.repo_name}"')
54 37
55 38 def test_my_account_my_watched(self, autologin_user):
56 39 response = self.app.get(route_path('my_account_watched'))
57 40
58 41 repos = UserFollowing.query().filter(
59 42 UserFollowing.user == User.get_by_username(
60 43 TEST_USER_ADMIN_LOGIN)).all()
61 44 for repo in repos:
62 45 response.mustcontain(f'"name_raw":"{repo.follows_repository.repo_name}"')
63 46
64 47 def test_my_account_perms(self, autologin_user):
65 48 response = self.app.get(route_path('my_account_perms'))
66 49 assert_response = response.assert_response()
67 50 assert assert_response.get_elements('.perm_tag.none')
68 51 assert assert_response.get_elements('.perm_tag.read')
69 52 assert assert_response.get_elements('.perm_tag.write')
70 53 assert assert_response.get_elements('.perm_tag.admin')
71 54
72 55 def test_my_account_notifications(self, autologin_user):
73 56 response = self.app.get(route_path('my_account_notifications'))
74 57 response.mustcontain('Test flash message')
@@ -1,164 +1,142 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 import pytest
21 20
22 21 from rhodecode.model.db import User, UserSshKeys
23 22
24 23 from rhodecode.tests import TestController, assert_session_flash
25 24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
26 26
27 27 fixture = Fixture()
28 28
29 29
30 def route_path(name, params=None, **kwargs):
31 import urllib.request
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'my_account_ssh_keys':
38 ADMIN_PREFIX + '/my_account/ssh_keys',
39 'my_account_ssh_keys_generate':
40 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
41 'my_account_ssh_keys_add':
42 ADMIN_PREFIX + '/my_account/ssh_keys/new',
43 'my_account_ssh_keys_delete':
44 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
45 }[name].format(**kwargs)
46
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
50
51
52 30 class TestMyAccountSshKeysView(TestController):
53 31 INVALID_KEY = """\
54 32 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 33 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 34 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 35 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 36 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 37 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 38 your_email@example.com
61 39 """
62 40 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 41 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 42 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 43 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 44 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 45 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 46 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 47 'your_email@example.com'
70 48 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71 49
72 50 def test_add_ssh_key_error(self, user_util):
73 51 user = user_util.create_user(password='qweqwe')
74 52 self.log_user(user.username, 'qweqwe')
75 53
76 54 key_data = self.INVALID_KEY
77 55
78 56 desc = 'MY SSH KEY'
79 57 response = self.app.post(
80 58 route_path('my_account_ssh_keys_add'),
81 59 {'description': desc, 'key_data': key_data,
82 60 'csrf_token': self.csrf_token})
83 61 assert_session_flash(response, 'An error occurred during ssh '
84 62 'key saving: Unable to decode the key')
85 63
86 64 def test_ssh_key_duplicate(self, user_util):
87 65 user = user_util.create_user(password='qweqwe')
88 66 self.log_user(user.username, 'qweqwe')
89 67 key_data = self.VALID_KEY
90 68
91 69 desc = 'MY SSH KEY'
92 70 response = self.app.post(
93 71 route_path('my_account_ssh_keys_add'),
94 72 {'description': desc, 'key_data': key_data,
95 73 'csrf_token': self.csrf_token})
96 74 assert_session_flash(response, 'Ssh Key successfully created')
97 75 response.follow() # flush session flash
98 76
99 77 # add the same key AGAIN
100 78 desc = 'MY SSH KEY'
101 79 response = self.app.post(
102 80 route_path('my_account_ssh_keys_add'),
103 81 {'description': desc, 'key_data': key_data,
104 82 'csrf_token': self.csrf_token})
105 83
106 84 err = 'Such key with fingerprint `{}` already exists, ' \
107 85 'please use a different one'.format(self.FINGERPRINT)
108 86 assert_session_flash(response, 'An error occurred during ssh key '
109 87 'saving: {}'.format(err))
110 88
111 89 def test_add_ssh_key(self, user_util):
112 90 user = user_util.create_user(password='qweqwe')
113 91 self.log_user(user.username, 'qweqwe')
114 92
115 93 key_data = self.VALID_KEY
116 94
117 95 desc = 'MY SSH KEY'
118 96 response = self.app.post(
119 97 route_path('my_account_ssh_keys_add'),
120 98 {'description': desc, 'key_data': key_data,
121 99 'csrf_token': self.csrf_token})
122 100 assert_session_flash(response, 'Ssh Key successfully created')
123 101
124 102 response = response.follow()
125 103 response.mustcontain(desc)
126 104
127 105 def test_delete_ssh_key(self, user_util):
128 106 user = user_util.create_user(password='qweqwe')
129 107 user_id = user.user_id
130 108 self.log_user(user.username, 'qweqwe')
131 109
132 110 key_data = self.VALID_KEY
133 111
134 112 desc = 'MY SSH KEY'
135 113 response = self.app.post(
136 114 route_path('my_account_ssh_keys_add'),
137 115 {'description': desc, 'key_data': key_data,
138 116 'csrf_token': self.csrf_token})
139 117 assert_session_flash(response, 'Ssh Key successfully created')
140 118 response = response.follow() # flush the Session flash
141 119
142 120 # now delete our key
143 121 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
144 122 assert 1 == len(keys)
145 123
146 124 response = self.app.post(
147 125 route_path('my_account_ssh_keys_delete'),
148 126 {'del_ssh_key': keys[0].ssh_key_id,
149 127 'csrf_token': self.csrf_token})
150 128
151 129 assert_session_flash(response, 'Ssh key successfully deleted')
152 130 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
153 131 assert 0 == len(keys)
154 132
155 133 def test_generate_keypair(self, user_util):
156 134 user = user_util.create_user(password='qweqwe')
157 135 self.log_user(user.username, 'qweqwe')
158 136
159 137 response = self.app.get(
160 138 route_path('my_account_ssh_keys_generate'))
161 139
162 140 response.mustcontain('Private key')
163 141 response.mustcontain('Public key')
164 142 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,89 +1,73 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests import assert_session_flash
22
23
24 def route_path(name, params=None, **kwargs):
25 import urllib.request
26 import urllib.parse
27 import urllib.error
28
29 base_url = {
30 'edit_repo_group_advanced':
31 '/{repo_group_name}/_settings/advanced',
32 'edit_repo_group_advanced_delete':
33 '/{repo_group_name}/_settings/advanced/delete',
34 }[name].format(**kwargs)
35
36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
22 from rhodecode.tests.routes import route_path
39 23
40 24
41 25 @pytest.mark.usefixtures("app")
42 26 class TestRepoGroupsAdvancedView(object):
43 27
44 28 @pytest.mark.parametrize('repo_group_name', [
45 29 'gro',
46 30 '12345',
47 31 ])
48 32 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
49 33 user_util._test_name = repo_group_name
50 34 gr = user_util.create_repo_group()
51 35 self.app.get(
52 36 route_path('edit_repo_group_advanced',
53 37 repo_group_name=gr.group_name))
54 38
55 39 def test_show_advanced_settings_delete(self, autologin_user, user_util,
56 40 csrf_token):
57 41 gr = user_util.create_repo_group(auto_cleanup=False)
58 42 repo_group_name = gr.group_name
59 43
60 44 params = dict(
61 45 csrf_token=csrf_token
62 46 )
63 47 response = self.app.post(
64 48 route_path('edit_repo_group_advanced_delete',
65 49 repo_group_name=repo_group_name), params=params)
66 50 assert_session_flash(
67 51 response, 'Removed repository group `{}`'.format(repo_group_name))
68 52
69 53 def test_delete_not_possible_with_objects_inside(self, autologin_user,
70 54 repo_groups, csrf_token):
71 55 zombie_group, parent_group, child_group = repo_groups
72 56
73 57 response = self.app.get(
74 58 route_path('edit_repo_group_advanced',
75 59 repo_group_name=parent_group.group_name))
76 60
77 61 response.mustcontain(
78 62 'This repository group includes 1 children repository group')
79 63
80 64 params = dict(
81 65 csrf_token=csrf_token
82 66 )
83 67 response = self.app.post(
84 68 route_path('edit_repo_group_advanced_delete',
85 69 repo_group_name=parent_group.group_name), params=params)
86 70
87 71 assert_session_flash(
88 72 response, 'This repository group contains 1 subgroup '
89 73 'and cannot be deleted')
@@ -1,86 +1,70 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests.utils import permission_update_data_generator
22
23
24 def route_path(name, params=None, **kwargs):
25 import urllib.request
26 import urllib.parse
27 import urllib.error
28
29 base_url = {
30 'edit_repo_group_perms':
31 '/{repo_group_name:}/_settings/permissions',
32 'edit_repo_group_perms_update':
33 '/{repo_group_name}/_settings/permissions/update',
34 }[name].format(**kwargs)
35
36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
22 from rhodecode.tests.routes import route_path
39 23
40 24
41 25 @pytest.mark.usefixtures("app")
42 26 class TestRepoGroupPermissionsView(object):
43 27
44 28 def test_edit_perms_view(self, user_util, autologin_user):
45 29 repo_group = user_util.create_repo_group()
46 30
47 31 self.app.get(
48 32 route_path('edit_repo_group_perms',
49 33 repo_group_name=repo_group.group_name), status=200)
50 34
51 35 def test_update_permissions(self, csrf_token, user_util):
52 36 repo_group = user_util.create_repo_group()
53 37 repo_group_name = repo_group.group_name
54 38 user = user_util.create_user()
55 39 user_id = user.user_id
56 40 username = user.username
57 41
58 42 # grant new
59 43 form_data = permission_update_data_generator(
60 44 csrf_token,
61 45 default='group.write',
62 46 grant=[(user_id, 'group.write', username, 'user')])
63 47
64 48 # recursive flag required for repo groups
65 49 form_data.extend([('recursive', u'none')])
66 50
67 51 response = self.app.post(
68 52 route_path('edit_repo_group_perms_update',
69 53 repo_group_name=repo_group_name), form_data).follow()
70 54
71 55 assert 'Repository Group permissions updated' in response
72 56
73 57 # revoke given
74 58 form_data = permission_update_data_generator(
75 59 csrf_token,
76 60 default='group.read',
77 61 revoke=[(user_id, 'user')])
78 62
79 63 # recursive flag required for repo groups
80 64 form_data.extend([('recursive', u'none')])
81 65
82 66 response = self.app.post(
83 67 route_path('edit_repo_group_perms_update',
84 68 repo_group_name=repo_group_name), form_data).follow()
85 69
86 70 assert 'Repository Group permissions updated' in response
@@ -1,91 +1,78 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import assert_session_flash
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
23 from rhodecode.tests.routes import route_path
29 24
30 base_url = {
31 'edit_repo_group': '/{repo_group_name}/_edit',
32 # Update is POST to the above url
33 }[name].format(**kwargs)
34
35 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
38 25
39 26
40 27 @pytest.mark.usefixtures("app")
41 28 class TestRepoGroupsSettingsView(object):
42 29
43 30 @pytest.mark.parametrize('repo_group_name', [
44 31 'gro',
45 32 u'12345',
46 33 ])
47 34 def test_edit(self, user_util, autologin_user, repo_group_name):
48 35 user_util._test_name = repo_group_name
49 36 repo_group = user_util.create_repo_group()
50 37
51 38 self.app.get(
52 39 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
53 40 status=200)
54 41
55 42 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
56 43 repo_group = user_util.create_repo_group()
57 44 repo_group_name = repo_group.group_name
58 45
59 46 description = 'description for newly created repo group'
60 47 form_data = rc_fixture._get_group_create_params(
61 48 group_name=repo_group.group_name,
62 49 group_description=description,
63 50 csrf_token=csrf_token,
64 51 repo_group_name=repo_group.group_name,
65 52 repo_group_owner=repo_group.user.username)
66 53
67 54 response = self.app.post(
68 55 route_path('edit_repo_group',
69 56 repo_group_name=repo_group.group_name),
70 57 form_data,
71 58 status=302)
72 59
73 60 assert_session_flash(
74 61 response, 'Repository Group `{}` updated successfully'.format(
75 62 repo_group_name))
76 63
77 64 def test_update_fails_when_parent_pointing_to_self(
78 65 self, csrf_token, user_util, autologin_user, rc_fixture):
79 66 group = user_util.create_repo_group()
80 67 response = self.app.post(
81 68 route_path('edit_repo_group', repo_group_name=group.group_name),
82 69 rc_fixture._get_group_create_params(
83 70 repo_group_name=group.group_name,
84 71 repo_group_owner=group.user.username,
85 72 repo_group=group.group_id,
86 73 csrf_token=csrf_token),
87 74 status=200
88 75 )
89 76 response.mustcontain(
90 77 '<span class="error-message">"{}" is not one of -1'.format(
91 78 group.group_id))
@@ -1,84 +1,69 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 from rhodecode.model.db import Repository
21
22
23 def route_path(name, params=None, **kwargs):
24 import urllib.request
25 import urllib.parse
26 import urllib.error
27
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 return base_url
20 from rhodecode.tests.routes import route_path
36 21
37 22
38 23 @pytest.mark.backends("git", "hg")
39 24 @pytest.mark.usefixtures('autologin_user', 'app')
40 25 class TestPullRequestList(object):
41 26
42 27 @pytest.mark.parametrize('params, expected_title', [
43 28 ({'source': 0, 'closed': 1}, 'Closed'),
44 29 ({'source': 0, 'my': 1}, 'Created by me'),
45 30 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 31 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 32 ({'source': 1}, 'From this repo'),
48 33 ])
49 34 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 35 pull_request = pr_util.create_pull_request()
51 36
52 37 response = self.app.get(
53 38 route_path('pullrequest_show_all',
54 39 repo_name=pull_request.target_repo.repo_name,
55 40 params=params))
56 41
57 42 assert_response = response.assert_response()
58 43
59 44 element = assert_response.get_element('.title .active')
60 45 element_text = element.text_content()
61 46 assert expected_title == element_text
62 47
63 48 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 49 pull_request = pr_util.create_pull_request()
65 50 response = self.app.get(
66 51 route_path('pullrequest_show_all_data',
67 52 repo_name=pull_request.target_repo.repo_name),
68 53 extra_environ=xhr_header)
69 54
70 55 assert response.json['recordsTotal'] == 1
71 56 assert response.json['data'][0]['description'] == 'Description'
72 57
73 58 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 59 xss_description = "<script>alert('Hi!')</script>"
75 60 pull_request = pr_util.create_pull_request(description=xss_description)
76 61
77 62 response = self.app.get(
78 63 route_path('pullrequest_show_all_data',
79 64 repo_name=pull_request.target_repo.repo_name),
80 65 extra_environ=xhr_header)
81 66
82 67 assert response.json['recordsTotal'] == 1
83 68 assert response.json['data'][0]['description'] == \
84 69 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,52 +1,40 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20 from rhodecode.model.db import Repository
21
22
23 def route_path(name, params=None, **kwargs):
24 import urllib.request
25 import urllib.parse
26 import urllib.error
21 from rhodecode.tests.routes import route_path
27 22
28 base_url = {
29 'bookmarks_home': '/{repo_name}/bookmarks',
30 }[name].format(**kwargs)
31
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 return base_url
35 23
36 24
37 25 @pytest.mark.usefixtures('autologin_user', 'app')
38 26 class TestBookmarks(object):
39 27
40 28 def test_index(self, backend):
41 29 if backend.alias == 'hg':
42 30 response = self.app.get(
43 31 route_path('bookmarks_home', repo_name=backend.repo_name))
44 32
45 33 repo = Repository.get_by_repo_name(backend.repo_name)
46 34 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
47 35 assert commit_id in response
48 36 assert obj_name in response
49 37 else:
50 38 self.app.get(
51 39 route_path('bookmarks_home', repo_name=backend.repo_name),
52 40 status=404)
@@ -1,48 +1,35 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20 from rhodecode.model.db import Repository
21
22
23 def route_path(name, params=None, **kwargs):
24 import urllib.request
25 import urllib.parse
26 import urllib.error
27
28 base_url = {
29 'branches_home': '/{repo_name}/branches',
30 }[name].format(**kwargs)
31
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 return base_url
21 from rhodecode.tests.routes import route_path
35 22
36 23
37 24 @pytest.mark.usefixtures('autologin_user', 'app')
38 25 class TestBranchesController(object):
39 26
40 27 def test_index(self, backend):
41 28 response = self.app.get(
42 29 route_path('branches_home', repo_name=backend.repo_name))
43 30
44 31 repo = Repository.get_by_repo_name(backend.repo_name)
45 32
46 33 for commit_id, obj_name in repo.scm_instance().branches.items():
47 34 assert commit_id in response
48 35 assert obj_name in response
@@ -1,219 +1,204 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import re
20 20
21 21 import pytest
22 22
23 23 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
24 24 from rhodecode.tests import TestController
25
26 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
25 from rhodecode.tests.routes import route_path
27 26
28 27
29 def route_path(name, params=None, **kwargs):
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'repo_changelog': '/{repo_name}/changelog',
36 'repo_commits': '/{repo_name}/commits',
37 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
38 'repo_commits_elements': '/{repo_name}/commits_elements',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
44 29
45 30
46 31 def assert_commits_on_page(response, indexes):
47 32 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.text)]
48 33 assert found_indexes == indexes
49 34
50 35
51 36 class TestChangelogController(TestController):
52 37
53 38 def test_commits_page(self, backend):
54 39 self.log_user()
55 40 response = self.app.get(
56 41 route_path('repo_commits', repo_name=backend.repo_name))
57 42
58 43 first_idx = -1
59 44 last_idx = -DEFAULT_CHANGELOG_SIZE
60 45 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
61 46
62 47 def test_changelog(self, backend):
63 48 self.log_user()
64 49 response = self.app.get(
65 50 route_path('repo_changelog', repo_name=backend.repo_name))
66 51
67 52 first_idx = -1
68 53 last_idx = -DEFAULT_CHANGELOG_SIZE
69 54 self.assert_commit_range_on_page(
70 55 response, first_idx, last_idx, backend)
71 56
72 57 @pytest.mark.backends("hg", "git")
73 58 def test_changelog_filtered_by_branch(self, backend):
74 59 self.log_user()
75 60 self.app.get(
76 61 route_path('repo_changelog', repo_name=backend.repo_name,
77 62 params=dict(branch=backend.default_branch_name)),
78 63 status=200)
79 64
80 65 @pytest.mark.backends("hg", "git")
81 66 def test_commits_filtered_by_branch(self, backend):
82 67 self.log_user()
83 68 self.app.get(
84 69 route_path('repo_commits', repo_name=backend.repo_name,
85 70 params=dict(branch=backend.default_branch_name)),
86 71 status=200)
87 72
88 73 @pytest.mark.backends("svn")
89 74 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
90 75 repo = backend['svn-simple-layout']
91 76 response = self.app.get(
92 77 route_path('repo_changelog', repo_name=repo.repo_name,
93 78 params=dict(branch='trunk')),
94 79 status=200)
95 80
96 81 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
97 82
98 83 def test_commits_filtered_by_wrong_branch(self, backend):
99 84 self.log_user()
100 85 branch = 'wrong-branch-name'
101 86 response = self.app.get(
102 87 route_path('repo_commits', repo_name=backend.repo_name,
103 88 params=dict(branch=branch)),
104 89 status=302)
105 90 expected_url = '/{repo}/commits/{branch}'.format(
106 91 repo=backend.repo_name, branch=branch)
107 92 assert expected_url in response.location
108 93 response = response.follow()
109 94 expected_warning = f'Branch {branch} is not found.'
110 95 assert expected_warning in response.text
111 96
112 97 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
113 98 def test_changelog_filtered_by_branch_with_merges(self, autologin_user, backend):
114 99
115 100 # Note: The changelog of branch "b" does not contain the commit "a1"
116 101 # although this is a parent of commit "b1". And branch "b" has commits
117 102 # which have a smaller index than commit "a1".
118 103 commits = [
119 104 {'message': 'a'},
120 105 {'message': 'b', 'branch': 'b'},
121 106 {'message': 'a1', 'parents': ['a']},
122 107 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
123 108 ]
124 109 backend.create_repo(commits)
125 110
126 111 self.app.get(
127 112 route_path('repo_changelog', repo_name=backend.repo_name,
128 113 params=dict(branch='b')),
129 114 status=200)
130 115
131 116 @pytest.mark.backends("hg")
132 117 def test_commits_closed_branches(self, autologin_user, backend):
133 118 repo = backend['closed_branch']
134 119 response = self.app.get(
135 120 route_path('repo_commits', repo_name=repo.repo_name,
136 121 params=dict(branch='experimental')),
137 122 status=200)
138 123
139 124 assert_commits_on_page(response, indexes=[3, 1])
140 125
141 126 def test_changelog_pagination(self, backend):
142 127 self.log_user()
143 128 # pagination, walk up to page 6
144 129 changelog_url = route_path(
145 130 'repo_commits', repo_name=backend.repo_name)
146 131
147 132 for page in range(1, 7):
148 133 response = self.app.get(changelog_url, {'page': page})
149 134
150 135 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
151 136 last_idx = -DEFAULT_CHANGELOG_SIZE * page
152 137 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
153 138
154 139 def assert_commit_range_on_page(
155 140 self, response, first_idx, last_idx, backend):
156 141 input_template = (
157 142 """<input class="commit-range" """
158 143 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
159 144 """data-short-id="%(short_id)s" id="%(raw_id)s" """
160 145 """name="%(raw_id)s" type="checkbox" value="1" />"""
161 146 )
162 147
163 148 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
164 149 repo = backend.repo
165 150
166 151 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
167 152 response.mustcontain(
168 153 input_template % {'raw_id': first_commit_on_page.raw_id,
169 154 'idx': first_commit_on_page.idx,
170 155 'short_id': first_commit_on_page.short_id})
171 156
172 157 response.mustcontain(commit_span_template % (
173 158 first_commit_on_page.idx, first_commit_on_page.short_id)
174 159 )
175 160
176 161 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 162 response.mustcontain(
178 163 input_template % {'raw_id': last_commit_on_page.raw_id,
179 164 'idx': last_commit_on_page.idx,
180 165 'short_id': last_commit_on_page.short_id})
181 166 response.mustcontain(commit_span_template % (
182 167 last_commit_on_page.idx, last_commit_on_page.short_id)
183 168 )
184 169
185 170 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
186 171 first_span_of_next_page = commit_span_template % (
187 172 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
188 173 assert first_span_of_next_page not in response
189 174
190 175 @pytest.mark.parametrize('test_path', [
191 176 'vcs/exceptions.py',
192 177 '/vcs/exceptions.py',
193 178 '//vcs/exceptions.py'
194 179 ])
195 180 def test_commits_with_filenode(self, backend, test_path):
196 181 self.log_user()
197 182 response = self.app.get(
198 183 route_path('repo_commits_file', repo_name=backend.repo_name,
199 184 commit_id='tip', f_path=test_path),
200 185 )
201 186
202 187 # history commits messages
203 188 response.mustcontain('Added exceptions module, this time for real')
204 189 response.mustcontain('Added not implemented hg backend test case')
205 190 response.mustcontain('Added BaseChangeset class')
206 191
207 192 def test_commits_with_filenode_that_is_dirnode(self, backend):
208 193 self.log_user()
209 194 self.app.get(
210 195 route_path('repo_commits_file', repo_name=backend.repo_name,
211 196 commit_id='tip', f_path='/tests'),
212 197 status=302)
213 198
214 199 def test_commits_with_filenode_not_existing(self, backend):
215 200 self.log_user()
216 201 self.app.get(
217 202 route_path('repo_commits_file', repo_name=backend.repo_name,
218 203 commit_id='tip', f_path='wrong_path'),
219 204 status=302)
@@ -1,494 +1,477 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests import TestController
22
22 from rhodecode.tests.routes import route_path
23 23 from rhodecode.model.db import ChangesetComment, Notification
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.lib import helpers as h
26 26
27 27
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
32
33 base_url = {
34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
44
45 28
46 29 @pytest.mark.backends("git", "hg", "svn")
47 30 class TestRepoCommitCommentsView(TestController):
48 31
49 32 @pytest.fixture(autouse=True)
50 33 def prepare(self, request, baseapp):
51 34 for x in ChangesetComment.query().all():
52 35 Session().delete(x)
53 36 Session().commit()
54 37
55 38 for x in Notification.query().all():
56 39 Session().delete(x)
57 40 Session().commit()
58 41
59 42 request.addfinalizer(self.cleanup)
60 43
61 44 def cleanup(self):
62 45 for x in ChangesetComment.query().all():
63 46 Session().delete(x)
64 47 Session().commit()
65 48
66 49 for x in Notification.query().all():
67 50 Session().delete(x)
68 51 Session().commit()
69 52
70 53 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 54 def test_create(self, comment_type, backend):
72 55 self.log_user()
73 56 commit = backend.repo.get_commit('300')
74 57 commit_id = commit.raw_id
75 58 text = 'CommentOnCommit'
76 59
77 60 params = {'text': text, 'csrf_token': self.csrf_token,
78 61 'comment_type': comment_type}
79 62 self.app.post(
80 63 route_path('repo_commit_comment_create',
81 64 repo_name=backend.repo_name, commit_id=commit_id),
82 65 params=params)
83 66
84 67 response = self.app.get(
85 68 route_path('repo_commit',
86 69 repo_name=backend.repo_name, commit_id=commit_id))
87 70
88 71 # test DB
89 72 assert ChangesetComment.query().count() == 1
90 73 assert_comment_links(response, ChangesetComment.query().count(), 0)
91 74
92 75 assert Notification.query().count() == 1
93 76 assert ChangesetComment.query().count() == 1
94 77
95 78 notification = Notification.query().all()[0]
96 79
97 80 comment_id = ChangesetComment.query().first().comment_id
98 81 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99 82
100 83 author = notification.created_by_user.username_and_name
101 84 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 85 author, comment_type, h.show_id(commit), backend.repo_name)
103 86 assert sbj == notification.subject
104 87
105 88 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
106 89 backend.repo_name, commit_id, comment_id))
107 90 assert lnk in notification.body
108 91
109 92 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 93 def test_create_inline(self, comment_type, backend):
111 94 self.log_user()
112 95 commit = backend.repo.get_commit('300')
113 96 commit_id = commit.raw_id
114 97 text = 'CommentOnCommit'
115 98 f_path = 'vcs/web/simplevcs/views/repository.py'
116 99 line = 'n1'
117 100
118 101 params = {'text': text, 'f_path': f_path, 'line': line,
119 102 'comment_type': comment_type,
120 103 'csrf_token': self.csrf_token}
121 104
122 105 self.app.post(
123 106 route_path('repo_commit_comment_create',
124 107 repo_name=backend.repo_name, commit_id=commit_id),
125 108 params=params)
126 109
127 110 response = self.app.get(
128 111 route_path('repo_commit',
129 112 repo_name=backend.repo_name, commit_id=commit_id))
130 113
131 114 # test DB
132 115 assert ChangesetComment.query().count() == 1
133 116 assert_comment_links(response, 0, ChangesetComment.query().count())
134 117
135 118 if backend.alias == 'svn':
136 119 response.mustcontain(
137 120 '''data-f-path="vcs/commands/summary.py" '''
138 121 '''data-anchor-id="c-300-ad05457a43f8"'''
139 122 )
140 123 if backend.alias == 'git':
141 124 response.mustcontain(
142 125 '''data-f-path="vcs/backends/hg.py" '''
143 126 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 127 )
145 128
146 129 if backend.alias == 'hg':
147 130 response.mustcontain(
148 131 '''data-f-path="vcs/backends/hg.py" '''
149 132 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 133 )
151 134
152 135 assert Notification.query().count() == 1
153 136 assert ChangesetComment.query().count() == 1
154 137
155 138 notification = Notification.query().all()[0]
156 139 comment = ChangesetComment.query().first()
157 140 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158 141
159 142 assert comment.revision == commit_id
160 143
161 144 author = notification.created_by_user.username_and_name
162 145 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 146 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164 147
165 148 assert sbj == notification.subject
166 149
167 150 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
168 151 backend.repo_name, commit_id, comment.comment_id))
169 152 assert lnk in notification.body
170 153 assert 'on line n1' in notification.body
171 154
172 155 def test_create_with_mention(self, backend):
173 156 self.log_user()
174 157
175 158 commit_id = backend.repo.get_commit('300').raw_id
176 159 text = '@test_regular check CommentOnCommit'
177 160
178 161 params = {'text': text, 'csrf_token': self.csrf_token}
179 162 self.app.post(
180 163 route_path('repo_commit_comment_create',
181 164 repo_name=backend.repo_name, commit_id=commit_id),
182 165 params=params)
183 166
184 167 response = self.app.get(
185 168 route_path('repo_commit',
186 169 repo_name=backend.repo_name, commit_id=commit_id))
187 170 # test DB
188 171 assert ChangesetComment.query().count() == 1
189 172 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 173
191 174 notification = Notification.query().one()
192 175
193 176 assert len(notification.recipients) == 2
194 177 users = [x.username for x in notification.recipients]
195 178
196 179 # test_regular gets notification by @mention
197 180 assert sorted(users) == ['test_admin', 'test_regular']
198 181
199 182 def test_create_with_status_change(self, backend):
200 183 self.log_user()
201 184 commit = backend.repo.get_commit('300')
202 185 commit_id = commit.raw_id
203 186 text = 'CommentOnCommit'
204 187 f_path = 'vcs/web/simplevcs/views/repository.py'
205 188 line = 'n1'
206 189
207 190 params = {'text': text, 'changeset_status': 'approved',
208 191 'csrf_token': self.csrf_token}
209 192
210 193 self.app.post(
211 194 route_path(
212 195 'repo_commit_comment_create',
213 196 repo_name=backend.repo_name, commit_id=commit_id),
214 197 params=params)
215 198
216 199 response = self.app.get(
217 200 route_path('repo_commit',
218 201 repo_name=backend.repo_name, commit_id=commit_id))
219 202
220 203 # test DB
221 204 assert ChangesetComment.query().count() == 1
222 205 assert_comment_links(response, ChangesetComment.query().count(), 0)
223 206
224 207 assert Notification.query().count() == 1
225 208 assert ChangesetComment.query().count() == 1
226 209
227 210 notification = Notification.query().all()[0]
228 211
229 212 comment_id = ChangesetComment.query().first().comment_id
230 213 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231 214
232 215 author = notification.created_by_user.username_and_name
233 216 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 217 author, h.show_id(commit), backend.repo_name)
235 218 assert sbj == notification.subject
236 219
237 220 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
238 221 backend.repo_name, commit_id, comment_id))
239 222 assert lnk in notification.body
240 223
241 224 def test_delete(self, backend):
242 225 self.log_user()
243 226 commit_id = backend.repo.get_commit('300').raw_id
244 227 text = 'CommentOnCommit'
245 228
246 229 params = {'text': text, 'csrf_token': self.csrf_token}
247 230 self.app.post(
248 231 route_path(
249 232 'repo_commit_comment_create',
250 233 repo_name=backend.repo_name, commit_id=commit_id),
251 234 params=params)
252 235
253 236 comments = ChangesetComment.query().all()
254 237 assert len(comments) == 1
255 238 comment_id = comments[0].comment_id
256 239
257 240 self.app.post(
258 241 route_path('repo_commit_comment_delete',
259 242 repo_name=backend.repo_name,
260 243 commit_id=commit_id,
261 244 comment_id=comment_id),
262 245 params={'csrf_token': self.csrf_token})
263 246
264 247 comments = ChangesetComment.query().all()
265 248 assert len(comments) == 0
266 249
267 250 response = self.app.get(
268 251 route_path('repo_commit',
269 252 repo_name=backend.repo_name, commit_id=commit_id))
270 253 assert_comment_links(response, 0, 0)
271 254
272 255 def test_edit(self, backend):
273 256 self.log_user()
274 257 commit_id = backend.repo.get_commit('300').raw_id
275 258 text = 'CommentOnCommit'
276 259
277 260 params = {'text': text, 'csrf_token': self.csrf_token}
278 261 self.app.post(
279 262 route_path(
280 263 'repo_commit_comment_create',
281 264 repo_name=backend.repo_name, commit_id=commit_id),
282 265 params=params)
283 266
284 267 comments = ChangesetComment.query().all()
285 268 assert len(comments) == 1
286 269 comment_id = comments[0].comment_id
287 270 test_text = 'test_text'
288 271 self.app.post(
289 272 route_path(
290 273 'repo_commit_comment_edit',
291 274 repo_name=backend.repo_name,
292 275 commit_id=commit_id,
293 276 comment_id=comment_id,
294 277 ),
295 278 params={
296 279 'csrf_token': self.csrf_token,
297 280 'text': test_text,
298 281 'version': '0',
299 282 })
300 283
301 284 text_form_db = ChangesetComment.query().filter(
302 285 ChangesetComment.comment_id == comment_id).first().text
303 286 assert test_text == text_form_db
304 287
305 288 def test_edit_without_change(self, backend):
306 289 self.log_user()
307 290 commit_id = backend.repo.get_commit('300').raw_id
308 291 text = 'CommentOnCommit'
309 292
310 293 params = {'text': text, 'csrf_token': self.csrf_token}
311 294 self.app.post(
312 295 route_path(
313 296 'repo_commit_comment_create',
314 297 repo_name=backend.repo_name, commit_id=commit_id),
315 298 params=params)
316 299
317 300 comments = ChangesetComment.query().all()
318 301 assert len(comments) == 1
319 302 comment_id = comments[0].comment_id
320 303
321 304 response = self.app.post(
322 305 route_path(
323 306 'repo_commit_comment_edit',
324 307 repo_name=backend.repo_name,
325 308 commit_id=commit_id,
326 309 comment_id=comment_id,
327 310 ),
328 311 params={
329 312 'csrf_token': self.csrf_token,
330 313 'text': text,
331 314 'version': '0',
332 315 },
333 316 status=404,
334 317 )
335 318 assert response.status_int == 404
336 319
337 320 def test_edit_try_edit_already_edited(self, backend):
338 321 self.log_user()
339 322 commit_id = backend.repo.get_commit('300').raw_id
340 323 text = 'CommentOnCommit'
341 324
342 325 params = {'text': text, 'csrf_token': self.csrf_token}
343 326 self.app.post(
344 327 route_path(
345 328 'repo_commit_comment_create',
346 329 repo_name=backend.repo_name, commit_id=commit_id
347 330 ),
348 331 params=params,
349 332 )
350 333
351 334 comments = ChangesetComment.query().all()
352 335 assert len(comments) == 1
353 336 comment_id = comments[0].comment_id
354 337 test_text = 'test_text'
355 338 self.app.post(
356 339 route_path(
357 340 'repo_commit_comment_edit',
358 341 repo_name=backend.repo_name,
359 342 commit_id=commit_id,
360 343 comment_id=comment_id,
361 344 ),
362 345 params={
363 346 'csrf_token': self.csrf_token,
364 347 'text': test_text,
365 348 'version': '0',
366 349 }
367 350 )
368 351 test_text_v2 = 'test_v2'
369 352 response = self.app.post(
370 353 route_path(
371 354 'repo_commit_comment_edit',
372 355 repo_name=backend.repo_name,
373 356 commit_id=commit_id,
374 357 comment_id=comment_id,
375 358 ),
376 359 params={
377 360 'csrf_token': self.csrf_token,
378 361 'text': test_text_v2,
379 362 'version': '0',
380 363 },
381 364 status=409,
382 365 )
383 366 assert response.status_int == 409
384 367
385 368 text_form_db = ChangesetComment.query().filter(
386 369 ChangesetComment.comment_id == comment_id).first().text
387 370
388 371 assert test_text == text_form_db
389 372 assert test_text_v2 != text_form_db
390 373
391 374 def test_edit_forbidden_for_immutable_comments(self, backend):
392 375 self.log_user()
393 376 commit_id = backend.repo.get_commit('300').raw_id
394 377 text = 'CommentOnCommit'
395 378
396 379 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 380 self.app.post(
398 381 route_path(
399 382 'repo_commit_comment_create',
400 383 repo_name=backend.repo_name,
401 384 commit_id=commit_id,
402 385 ),
403 386 params=params
404 387 )
405 388
406 389 comments = ChangesetComment.query().all()
407 390 assert len(comments) == 1
408 391 comment_id = comments[0].comment_id
409 392
410 393 comment = ChangesetComment.get(comment_id)
411 394 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 395 Session().add(comment)
413 396 Session().commit()
414 397
415 398 response = self.app.post(
416 399 route_path(
417 400 'repo_commit_comment_edit',
418 401 repo_name=backend.repo_name,
419 402 commit_id=commit_id,
420 403 comment_id=comment_id,
421 404 ),
422 405 params={
423 406 'csrf_token': self.csrf_token,
424 407 'text': 'test_text',
425 408 },
426 409 status=403,
427 410 )
428 411 assert response.status_int == 403
429 412
430 413 def test_delete_forbidden_for_immutable_comments(self, backend):
431 414 self.log_user()
432 415 commit_id = backend.repo.get_commit('300').raw_id
433 416 text = 'CommentOnCommit'
434 417
435 418 params = {'text': text, 'csrf_token': self.csrf_token}
436 419 self.app.post(
437 420 route_path(
438 421 'repo_commit_comment_create',
439 422 repo_name=backend.repo_name, commit_id=commit_id),
440 423 params=params)
441 424
442 425 comments = ChangesetComment.query().all()
443 426 assert len(comments) == 1
444 427 comment_id = comments[0].comment_id
445 428
446 429 comment = ChangesetComment.get(comment_id)
447 430 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 431 Session().add(comment)
449 432 Session().commit()
450 433
451 434 self.app.post(
452 435 route_path('repo_commit_comment_delete',
453 436 repo_name=backend.repo_name,
454 437 commit_id=commit_id,
455 438 comment_id=comment_id),
456 439 params={'csrf_token': self.csrf_token},
457 440 status=403)
458 441
459 442 @pytest.mark.parametrize('renderer, text_input, output', [
460 443 ('rst', 'plain text', '<p>plain text</p>'),
461 444 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 445 ('rst', '*italics*', '<em>italics</em>'),
463 446 ('rst', '**bold**', '<strong>bold</strong>'),
464 447 ('markdown', 'plain text', '<p>plain text</p>'),
465 448 ('markdown', '# header', '<h1>header</h1>'),
466 449 ('markdown', '*italics*', '<em>italics</em>'),
467 450 ('markdown', '**bold**', '<strong>bold</strong>'),
468 451 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 452 'md-header', 'md-italics', 'md-bold', ])
470 453 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 454 self.log_user()
472 455 params = {
473 456 'renderer': renderer,
474 457 'text': text_input,
475 458 'csrf_token': self.csrf_token
476 459 }
477 460 commit_id = '0' * 16 # fake this for tests
478 461 response = self.app.post(
479 462 route_path('repo_commit_comment_preview',
480 463 repo_name=backend.repo_name, commit_id=commit_id,),
481 464 params=params,
482 465 extra_environ=xhr_header)
483 466
484 467 response.mustcontain(output)
485 468
486 469
487 470 def assert_comment_links(response, comments, inline_comments):
488 471 response.mustcontain(
489 472 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 473 response.mustcontain(
491 474 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
492 475
493 476
494 477
@@ -1,336 +1,316 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 23 from rhodecode.lib.helpers import _shorten_commit_id
24
25
26 def route_path(name, params=None, **kwargs):
27 import urllib.request
28 import urllib.parse
29 import urllib.error
30
31 base_url = {
32 'repo_commit': '/{repo_name}/changeset/{commit_id}',
33 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
34 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
35 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
36 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
37 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
38 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
39 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
40 }[name].format(**kwargs)
41
42 if params:
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 return base_url
24 from rhodecode.tests.routes import route_path
45 25
46 26
47 27 @pytest.mark.usefixtures("app")
48 28 class TestRepoCommitView(object):
49 29
50 30 def test_show_commit(self, backend):
51 31 commit_id = self.commit_id[backend.alias]
52 32 response = self.app.get(route_path(
53 33 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
54 34 response.mustcontain('Added a symlink')
55 35 response.mustcontain(commit_id)
56 36 response.mustcontain('No newline at end of file')
57 37
58 38 def test_show_raw(self, backend):
59 39 commit_id = self.commit_id[backend.alias]
60 40 # webtest uses linter to check if response is bytes,
61 41 # and we use memoryview here as a wrapper, quick turn-off
62 42 self.app.lint = False
63 43
64 44 response = self.app.get(route_path(
65 45 'repo_commit_raw',
66 46 repo_name=backend.repo_name, commit_id=commit_id))
67 47 assert response.body == self.diffs[backend.alias]
68 48
69 49 def test_show_raw_patch(self, backend):
70 50 response = self.app.get(route_path(
71 51 'repo_commit_patch', repo_name=backend.repo_name,
72 52 commit_id=self.commit_id[backend.alias]))
73 53 assert response.body == self.patches[backend.alias]
74 54
75 55 def test_commit_download(self, backend):
76 56 # webtest uses linter to check if response is bytes,
77 57 # and we use memoryview here as a wrapper, quick turn-off
78 58 self.app.lint = False
79 59
80 60 response = self.app.get(route_path(
81 61 'repo_commit_download',
82 62 repo_name=backend.repo_name,
83 63 commit_id=self.commit_id[backend.alias]))
84 64 assert response.body == self.diffs[backend.alias]
85 65
86 66 def test_single_commit_page_different_ops(self, backend):
87 67 commit_id = {
88 68 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
89 69 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
90 70 'svn': '337',
91 71 }
92 72 diff_stat = {
93 73 'hg': (21, 943, 288),
94 74 'git': (20, 941, 286),
95 75 'svn': (21, 943, 288),
96 76 }
97 77
98 78 commit_id = commit_id[backend.alias]
99 79 response = self.app.get(route_path(
100 80 'repo_commit',
101 81 repo_name=backend.repo_name, commit_id=commit_id))
102 82
103 83 response.mustcontain(_shorten_commit_id(commit_id))
104 84
105 85 compare_page = ComparePage(response)
106 86 file_changes = diff_stat[backend.alias]
107 87 compare_page.contains_change_summary(*file_changes)
108 88
109 89 # files op files
110 90 response.mustcontain('File not present at commit: %s' %
111 91 _shorten_commit_id(commit_id))
112 92
113 93 # svn uses a different filename
114 94 if backend.alias == 'svn':
115 95 response.mustcontain('new file 10644')
116 96 else:
117 97 response.mustcontain('new file 100644')
118 98 response.mustcontain('Changed theme to ADC theme') # commit msg
119 99
120 100 self._check_new_diff_menus(response, right_menu=True)
121 101
122 102 def test_commit_range_page_different_ops(self, backend):
123 103 commit_id_range = {
124 104 'hg': (
125 105 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
126 106 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
127 107 'git': (
128 108 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
129 109 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
130 110 'svn': (
131 111 '335',
132 112 '337'),
133 113 }
134 114 commit_ids = commit_id_range[backend.alias]
135 115 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
136 116 response = self.app.get(route_path(
137 117 'repo_commit',
138 118 repo_name=backend.repo_name, commit_id=commit_id))
139 119
140 120 response.mustcontain(_shorten_commit_id(commit_ids[0]))
141 121 response.mustcontain(_shorten_commit_id(commit_ids[1]))
142 122
143 123 compare_page = ComparePage(response)
144 124
145 125 # svn is special
146 126 if backend.alias == 'svn':
147 127 response.mustcontain('new file 10644')
148 128 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
149 129 compare_page.contains_change_summary(*file_changes)
150 130 elif backend.alias == 'git':
151 131 response.mustcontain('new file 100644')
152 132 for file_changes in [(12, 222, 20), (20, 941, 286)]:
153 133 compare_page.contains_change_summary(*file_changes)
154 134 else:
155 135 response.mustcontain('new file 100644')
156 136 for file_changes in [(12, 222, 20), (21, 943, 288)]:
157 137 compare_page.contains_change_summary(*file_changes)
158 138
159 139 # files op files
160 140 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
161 141 response.mustcontain('Added docstrings to vcs.cli') # commit msg
162 142 response.mustcontain('Changed theme to ADC theme') # commit msg
163 143
164 144 self._check_new_diff_menus(response)
165 145
166 146 def test_combined_compare_commit_page_different_ops(self, backend):
167 147 commit_id_range = {
168 148 'hg': (
169 149 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
170 150 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
171 151 'git': (
172 152 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
173 153 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
174 154 'svn': (
175 155 '335',
176 156 '337'),
177 157 }
178 158 commit_ids = commit_id_range[backend.alias]
179 159 response = self.app.get(route_path(
180 160 'repo_compare',
181 161 repo_name=backend.repo_name,
182 162 source_ref_type='rev', source_ref=commit_ids[0],
183 163 target_ref_type='rev', target_ref=commit_ids[1], ))
184 164
185 165 response.mustcontain(_shorten_commit_id(commit_ids[0]))
186 166 response.mustcontain(_shorten_commit_id(commit_ids[1]))
187 167
188 168 # files op files
189 169 response.mustcontain('File not present at commit: %s' %
190 170 _shorten_commit_id(commit_ids[1]))
191 171
192 172 compare_page = ComparePage(response)
193 173
194 174 # svn is special
195 175 if backend.alias == 'svn':
196 176 response.mustcontain('new file 10644')
197 177 file_changes = (32, 1179, 310)
198 178 compare_page.contains_change_summary(*file_changes)
199 179 elif backend.alias == 'git':
200 180 response.mustcontain('new file 100644')
201 181 file_changes = (31, 1163, 306)
202 182 compare_page.contains_change_summary(*file_changes)
203 183 else:
204 184 response.mustcontain('new file 100644')
205 185 file_changes = (32, 1165, 308)
206 186 compare_page.contains_change_summary(*file_changes)
207 187
208 188 response.mustcontain('Added docstrings to vcs.cli') # commit msg
209 189 response.mustcontain('Changed theme to ADC theme') # commit msg
210 190
211 191 self._check_new_diff_menus(response)
212 192
213 193 def test_changeset_range(self, backend):
214 194 self._check_changeset_range(
215 195 backend, self.commit_id_range, self.commit_id_range_result)
216 196
217 197 def test_changeset_range_with_initial_commit(self, backend):
218 198 commit_id_range = {
219 199 'hg': (
220 200 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
221 201 '...6cba7170863a2411822803fa77a0a264f1310b35'),
222 202 'git': (
223 203 'c1214f7e79e02fc37156ff215cd71275450cffc3'
224 204 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
225 205 'svn': '1...3',
226 206 }
227 207 commit_id_range_result = {
228 208 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
229 209 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
230 210 'svn': ['1', '2', '3'],
231 211 }
232 212 self._check_changeset_range(
233 213 backend, commit_id_range, commit_id_range_result)
234 214
235 215 def _check_changeset_range(
236 216 self, backend, commit_id_ranges, commit_id_range_result):
237 217 response = self.app.get(
238 218 route_path('repo_commit',
239 219 repo_name=backend.repo_name,
240 220 commit_id=commit_id_ranges[backend.alias]))
241 221
242 222 expected_result = commit_id_range_result[backend.alias]
243 223 response.mustcontain('{} commits'.format(len(expected_result)))
244 224 for commit_id in expected_result:
245 225 response.mustcontain(commit_id)
246 226
247 227 commit_id = {
248 228 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
249 229 'svn': '393',
250 230 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
251 231 }
252 232
253 233 commit_id_range = {
254 234 'hg': (
255 235 'a53d9201d4bc278910d416d94941b7ea007ecd52'
256 236 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
257 237 'git': (
258 238 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
259 239 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
260 240 'svn': '391...393',
261 241 }
262 242
263 243 commit_id_range_result = {
264 244 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
265 245 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
266 246 'svn': ['391', '392', '393'],
267 247 }
268 248
269 249 diffs = {
270 250 'hg': br"""diff --git a/README b/README
271 251 new file mode 120000
272 252 --- /dev/null
273 253 +++ b/README
274 254 @@ -0,0 +1,1 @@
275 255 +README.rst
276 256 \ No newline at end of file
277 257 """,
278 258 'git': br"""diff --git a/README b/README
279 259 new file mode 120000
280 260 index 0000000..92cacd2
281 261 --- /dev/null
282 262 +++ b/README
283 263 @@ -0,0 +1 @@
284 264 +README.rst
285 265 \ No newline at end of file
286 266 """,
287 267 'svn': b"""Index: README
288 268 ===================================================================
289 269 diff --git a/README b/README
290 270 new file mode 10644
291 271 --- /dev/null\t(revision 0)
292 272 +++ b/README\t(revision 393)
293 273 @@ -0,0 +1 @@
294 274 +link README.rst
295 275 \\ No newline at end of file
296 276 """,
297 277 }
298 278
299 279 patches = {
300 280 'hg': br"""# HG changeset patch
301 281 # User Marcin Kuzminski <marcin@python-works.com>
302 282 # Date 2014-01-07 12:21:40
303 283 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
304 284 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
305 285
306 286 Added a symlink
307 287
308 288 """ + diffs['hg'],
309 289 'git': br"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
310 290 From: Marcin Kuzminski <marcin@python-works.com>
311 291 Date: 2014-01-07 12:22:20
312 292 Subject: [PATCH] Added a symlink
313 293
314 294 ---
315 295
316 296 """ + diffs['git'],
317 297 'svn': br"""# SVN changeset patch
318 298 # User marcin
319 299 # Date 2014-09-02 12:25:22.071142
320 300 # Revision 393
321 301
322 302 Added a symlink
323 303
324 304 """ + diffs['svn'],
325 305 }
326 306
327 307 def _check_new_diff_menus(self, response, right_menu=False,):
328 308 # individual file diff menus
329 309 for elem in ['Show file before', 'Show file after']:
330 310 response.mustcontain(elem)
331 311
332 312 # right pane diff menus
333 313 if right_menu:
334 314 for elem in ['Hide whitespace changes', 'Toggle wide diff',
335 315 'Show full context diff']:
336 316 response.mustcontain(elem)
@@ -1,670 +1,656 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22 import lxml.html
23 23
24 24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 25 from rhodecode.tests import assert_session_flash
26 26 from rhodecode.tests.utils import AssertResponse, commit_change
27
28
29 def route_path(name, params=None, **kwargs):
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'repo_compare_select': '/{repo_name}/compare',
36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 }[name].format(**kwargs)
38
39 if params:
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
27 from rhodecode.tests.routes import route_path
42 28
43 29
44 30 @pytest.mark.usefixtures("autologin_user", "app")
45 31 class TestCompareView(object):
46 32
47 33 def test_compare_index_is_reached_at_least_once(self, backend):
48 34 repo = backend.repo
49 35 self.app.get(
50 36 route_path('repo_compare_select', repo_name=repo.repo_name))
51 37
52 38 @pytest.mark.xfail_backends("svn", reason="Requires pull")
53 39 def test_compare_remote_with_different_commit_indexes(self, backend):
54 40 # Preparing the following repository structure:
55 41 #
56 42 # Origin repository has two commits:
57 43 #
58 44 # 0 1
59 45 # A -- D
60 46 #
61 47 # The fork of it has a few more commits and "D" has a commit index
62 48 # which does not exist in origin.
63 49 #
64 50 # 0 1 2 3 4
65 51 # A -- -- -- D -- E
66 52 # \- B -- C
67 53 #
68 54
69 55 fork = backend.create_repo()
70 56 origin = backend.create_repo()
71 57
72 58 # prepare fork
73 59 commit0 = commit_change(
74 60 fork.repo_name, filename=b'file1', content=b'A',
75 61 message='A - Initial Commit', vcs_type=backend.alias, parent=None, newfile=True)
76 62
77 63 commit1 = commit_change(
78 64 fork.repo_name, filename=b'file1', content=b'B',
79 65 message='B, child of A', vcs_type=backend.alias, parent=commit0)
80 66
81 67 commit_change( # commit 2
82 68 fork.repo_name, filename=b'file1', content=b'C',
83 69 message='C, child of B', vcs_type=backend.alias, parent=commit1)
84 70
85 71 commit3 = commit_change(
86 72 fork.repo_name, filename=b'file1', content=b'D',
87 73 message='D, child of A', vcs_type=backend.alias, parent=commit0)
88 74
89 75 commit4 = commit_change(
90 76 fork.repo_name, filename=b'file1', content=b'E',
91 77 message='E, child of D', vcs_type=backend.alias, parent=commit3)
92 78
93 79 # prepare origin repository, taking just the history up to D
94 80
95 81 origin_repo = origin.scm_instance(cache=False)
96 82 origin_repo.config.clear_section('hooks')
97 83 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
98 84 origin_repo = origin.scm_instance(cache=False) # cache rebuild
99 85
100 86 # Verify test fixture setup
101 87 # This does not work for git
102 88 if backend.alias != 'git':
103 89 assert 5 == len(fork.scm_instance(cache=False).commit_ids)
104 90 assert 2 == len(origin_repo.commit_ids)
105 91
106 92 # Comparing the revisions
107 93 response = self.app.get(
108 94 route_path('repo_compare',
109 95 repo_name=origin.repo_name,
110 96 source_ref_type="rev", source_ref=commit3.raw_id,
111 97 target_ref_type="rev", target_ref=commit4.raw_id,
112 98 params=dict(merge='1', target_repo=fork.repo_name)
113 99 ),
114 100 status=200)
115 101
116 102 compare_page = ComparePage(response)
117 103 compare_page.contains_commits([commit4])
118 104
119 105 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
120 106 def test_compare_forks_on_branch_extra_commits(self, backend):
121 107 repo1 = backend.create_repo()
122 108
123 109 # commit something !
124 110 commit0 = commit_change(
125 111 repo1.repo_name, filename=b'file1', content=b'line1\n',
126 112 message='commit1', vcs_type=backend.alias, parent=None,
127 113 newfile=True)
128 114
129 115 # fork this repo
130 116 repo2 = backend.create_fork()
131 117
132 118 # add two extra commit into fork
133 119 commit1 = commit_change(
134 120 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
135 121 message='commit2', vcs_type=backend.alias, parent=commit0)
136 122
137 123 commit2 = commit_change(
138 124 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
139 125 message='commit3', vcs_type=backend.alias, parent=commit1)
140 126
141 127 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
142 128 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
143 129
144 130 response = self.app.get(
145 131 route_path('repo_compare',
146 132 repo_name=repo1.repo_name,
147 133 source_ref_type="branch", source_ref=commit_id2,
148 134 target_ref_type="branch", target_ref=commit_id1,
149 135 params=dict(merge='1', target_repo=repo2.repo_name)
150 136 ))
151 137
152 138 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
153 139 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
154 140
155 141 compare_page = ComparePage(response)
156 142 compare_page.contains_change_summary(1, 2, 0)
157 143 compare_page.contains_commits([commit1, commit2])
158 144
159 145 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
160 146 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
161 147
162 148 # Swap is removed when comparing branches since it's a PR feature and
163 149 # it is then a preview mode
164 150 compare_page.swap_is_hidden()
165 151 compare_page.target_source_are_disabled()
166 152
167 153 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
168 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
154 def test_compare_forks_on_branch_extra_commits_origin_has_incoming(self, backend):
169 155 repo1 = backend.create_repo()
170 156
171 157 # commit something !
172 158 commit0 = commit_change(
173 159 repo1.repo_name, filename=b'file1', content=b'line1\n',
174 160 message='commit1', vcs_type=backend.alias, parent=None,
175 161 newfile=True)
176 162
177 163 # fork this repo
178 164 repo2 = backend.create_fork()
179 165
180 166 # now commit something to origin repo
181 167 commit_change(
182 168 repo1.repo_name, filename=b'file2', content=b'line1file2\n',
183 169 message='commit2', vcs_type=backend.alias, parent=commit0,
184 170 newfile=True)
185 171
186 172 # add two extra commit into fork
187 173 commit1 = commit_change(
188 174 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
189 175 message='commit2', vcs_type=backend.alias, parent=commit0)
190 176
191 177 commit2 = commit_change(
192 178 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
193 179 message='commit3', vcs_type=backend.alias, parent=commit1)
194 180
195 181 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
196 182 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
197 183
198 184 response = self.app.get(
199 185 route_path('repo_compare',
200 186 repo_name=repo1.repo_name,
201 187 source_ref_type="branch", source_ref=commit_id2,
202 188 target_ref_type="branch", target_ref=commit_id1,
203 189 params=dict(merge='1', target_repo=repo2.repo_name),
204 190 ))
205 191
206 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
207 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
192 response.mustcontain(f'{repo1.repo_name}@{commit_id2}')
193 response.mustcontain(f'{repo2.repo_name}@{commit_id1}')
208 194
209 195 compare_page = ComparePage(response)
210 196 compare_page.contains_change_summary(1, 2, 0)
211 197 compare_page.contains_commits([commit1, commit2])
212 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
198 anchor = f'a_c-{commit0.short_id}-826e8142e6ba'
213 199 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
214 200
215 201 # Swap is removed when comparing branches since it's a PR feature and
216 202 # it is then a preview mode
217 203 compare_page.swap_is_hidden()
218 204 compare_page.target_source_are_disabled()
219 205
220 206 @pytest.mark.xfail_backends("svn")
221 207 # TODO(marcink): no svn support for compare two seperate repos
222 208 def test_compare_of_unrelated_forks(self, backend):
223 209 orig = backend.create_repo(number_of_commits=1)
224 210 fork = backend.create_repo(number_of_commits=1)
225 211
226 212 response = self.app.get(
227 213 route_path('repo_compare',
228 214 repo_name=orig.repo_name,
229 215 source_ref_type="rev", source_ref="tip",
230 216 target_ref_type="rev", target_ref="tip",
231 217 params=dict(merge='1', target_repo=fork.repo_name),
232 218 ),
233 219 status=302)
234 220 response = response.follow()
235 221 response.mustcontain("Repositories unrelated.")
236 222
237 223 @pytest.mark.xfail_backends("svn")
238 224 def test_compare_cherry_pick_commits_from_bottom(self, backend):
239 225
240 226 # repo1:
241 227 # commit0:
242 228 # commit1:
243 229 # repo1-fork- in which we will cherry pick bottom commits
244 230 # commit0:
245 231 # commit1:
246 232 # commit2: x
247 233 # commit3: x
248 234 # commit4: x
249 235 # commit5:
250 236 # make repo1, and commit1+commit2
251 237
252 238 repo1 = backend.create_repo()
253 239
254 240 # commit something !
255 241 commit0 = commit_change(
256 242 repo1.repo_name, filename=b'file1', content=b'line1\n',
257 243 message='commit1', vcs_type=backend.alias, parent=None,
258 244 newfile=True)
259 245 commit1 = commit_change(
260 246 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
261 247 message='commit2', vcs_type=backend.alias, parent=commit0)
262 248
263 249 # fork this repo
264 250 repo2 = backend.create_fork()
265 251
266 252 # now make commit3-6
267 253 commit2 = commit_change(
268 254 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
269 255 message='commit3', vcs_type=backend.alias, parent=commit1)
270 256 commit3 = commit_change(
271 257 repo1.repo_name, filename=b'file1',content=b'line1\nline2\nline3\nline4\n',
272 258 message='commit4', vcs_type=backend.alias, parent=commit2)
273 259 commit4 = commit_change(
274 260 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\n',
275 261 message='commit5', vcs_type=backend.alias, parent=commit3)
276 262 commit_change( # commit 5
277 263 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
278 264 message='commit6', vcs_type=backend.alias, parent=commit4)
279 265
280 266 response = self.app.get(
281 267 route_path('repo_compare',
282 268 repo_name=repo2.repo_name,
283 269 # parent of commit2, in target repo2
284 270 source_ref_type="rev", source_ref=commit1.raw_id,
285 271 target_ref_type="rev", target_ref=commit4.raw_id,
286 272 params=dict(merge='1', target_repo=repo1.repo_name),
287 273 ))
288 274 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
289 275 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
290 276
291 277 # files
292 278 compare_page = ComparePage(response)
293 279 compare_page.contains_change_summary(1, 3, 0)
294 280 compare_page.contains_commits([commit2, commit3, commit4])
295 281 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
296 282 compare_page.contains_file_links_and_anchors([('file1', anchor),])
297 283
298 284 @pytest.mark.xfail_backends("svn")
299 285 def test_compare_cherry_pick_commits_from_top(self, backend):
300 286 # repo1:
301 287 # commit0:
302 288 # commit1:
303 289 # repo1-fork- in which we will cherry pick bottom commits
304 290 # commit0:
305 291 # commit1:
306 292 # commit2:
307 293 # commit3: x
308 294 # commit4: x
309 295 # commit5: x
310 296
311 297 # make repo1, and commit1+commit2
312 298 repo1 = backend.create_repo()
313 299
314 300 # commit something !
315 301 commit0 = commit_change(
316 302 repo1.repo_name, filename=b'file1', content=b'line1\n',
317 303 message='commit1', vcs_type=backend.alias, parent=None,
318 304 newfile=True)
319 305 commit1 = commit_change(
320 306 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
321 307 message='commit2', vcs_type=backend.alias, parent=commit0)
322 308
323 309 # fork this repo
324 310 backend.create_fork()
325 311
326 312 # now make commit3-6
327 313 commit2 = commit_change(
328 314 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
329 315 message='commit3', vcs_type=backend.alias, parent=commit1)
330 316 commit3 = commit_change(
331 317 repo1.repo_name, filename=b'file1',
332 318 content=b'line1\nline2\nline3\nline4\n', message='commit4',
333 319 vcs_type=backend.alias, parent=commit2)
334 320 commit4 = commit_change(
335 321 repo1.repo_name, filename=b'file1',
336 322 content=b'line1\nline2\nline3\nline4\nline5\n', message='commit5',
337 323 vcs_type=backend.alias, parent=commit3)
338 324 commit5 = commit_change(
339 325 repo1.repo_name, filename=b'file1',
340 326 content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
341 327 message='commit6', vcs_type=backend.alias, parent=commit4)
342 328
343 329 response = self.app.get(
344 330 route_path('repo_compare',
345 331 repo_name=repo1.repo_name,
346 332 # parent of commit3, not in source repo2
347 333 source_ref_type="rev", source_ref=commit2.raw_id,
348 334 target_ref_type="rev", target_ref=commit5.raw_id,
349 335 params=dict(merge='1'),))
350 336
351 337 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
352 338 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
353 339
354 340 compare_page = ComparePage(response)
355 341 compare_page.contains_change_summary(1, 3, 0)
356 342 compare_page.contains_commits([commit3, commit4, commit5])
357 343
358 344 # files
359 345 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
360 346 compare_page.contains_file_links_and_anchors([('file1', anchor),])
361 347
362 348 @pytest.mark.xfail_backends("svn")
363 349 def test_compare_remote_branches(self, backend):
364 350 repo1 = backend.repo
365 351 repo2 = backend.create_fork()
366 352
367 353 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
368 354 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
369 355 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
370 356 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
371 357
372 358 response = self.app.get(
373 359 route_path('repo_compare',
374 360 repo_name=repo1.repo_name,
375 361 source_ref_type="rev", source_ref=commit_id1,
376 362 target_ref_type="rev", target_ref=commit_id2,
377 363 params=dict(merge='1', target_repo=repo2.repo_name),
378 364 ))
379 365
380 366 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
381 367 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
382 368
383 369 compare_page = ComparePage(response)
384 370
385 371 # outgoing commits between those commits
386 372 compare_page.contains_commits(
387 373 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
388 374
389 375 # files
390 376 compare_page.contains_file_links_and_anchors([
391 377 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
392 378 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
393 379 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
394 380 ])
395 381
396 382 @pytest.mark.xfail_backends("svn")
397 383 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
398 384 repo1 = backend.create_repo()
399 385 r1_name = repo1.repo_name
400 386
401 387 commit0 = commit_change(
402 388 repo=r1_name, filename=b'file1',
403 389 content=b'line1', message='commit1', vcs_type=backend.alias,
404 390 newfile=True)
405 391 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
406 392
407 393 # fork the repo1
408 394 repo2 = backend.create_fork()
409 395 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
410 396
411 397 self.r2_id = repo2.repo_id
412 398 r2_name = repo2.repo_name
413 399
414 400 commit1 = commit_change(
415 401 repo=r2_name, filename=b'file1-fork',
416 402 content=b'file1-line1-from-fork', message='commit1-fork',
417 403 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
418 404 newfile=True)
419 405
420 406 commit2 = commit_change(
421 407 repo=r2_name, filename=b'file2-fork',
422 408 content=b'file2-line1-from-fork', message='commit2-fork',
423 409 vcs_type=backend.alias, parent=commit1,
424 410 newfile=True)
425 411
426 412 commit_change( # commit 3
427 413 repo=r2_name, filename=b'file3-fork',
428 414 content=b'file3-line1-from-fork', message='commit3-fork',
429 415 vcs_type=backend.alias, parent=commit2, newfile=True)
430 416
431 417 # compare !
432 418 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
433 419 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
434 420
435 421 response = self.app.get(
436 422 route_path('repo_compare',
437 423 repo_name=r2_name,
438 424 source_ref_type="branch", source_ref=commit_id1,
439 425 target_ref_type="branch", target_ref=commit_id2,
440 426 params=dict(merge='1', target_repo=r1_name),
441 427 ))
442 428
443 429 response.mustcontain('%s@%s' % (r2_name, commit_id1))
444 430 response.mustcontain('%s@%s' % (r1_name, commit_id2))
445 431 response.mustcontain('No files')
446 432 response.mustcontain('No commits in this compare')
447 433
448 434 commit0 = commit_change(
449 435 repo=r1_name, filename=b'file2',
450 436 content=b'line1-added-after-fork', message='commit2-parent',
451 437 vcs_type=backend.alias, parent=None, newfile=True)
452 438
453 439 # compare !
454 440 response = self.app.get(
455 441 route_path('repo_compare',
456 442 repo_name=r2_name,
457 443 source_ref_type="branch", source_ref=commit_id1,
458 444 target_ref_type="branch", target_ref=commit_id2,
459 445 params=dict(merge='1', target_repo=r1_name),
460 446 ))
461 447
462 448 response.mustcontain('%s@%s' % (r2_name, commit_id1))
463 449 response.mustcontain('%s@%s' % (r1_name, commit_id2))
464 450
465 451 response.mustcontain("""commit2-parent""")
466 452 response.mustcontain("""line1-added-after-fork""")
467 453 compare_page = ComparePage(response)
468 454 compare_page.contains_change_summary(1, 1, 0)
469 455
470 456 @pytest.mark.xfail_backends("svn")
471 457 def test_compare_commits(self, backend, xhr_header):
472 458 commit0 = backend.repo.get_commit(commit_idx=0)
473 459 commit1 = backend.repo.get_commit(commit_idx=1)
474 460
475 461 response = self.app.get(
476 462 route_path('repo_compare',
477 463 repo_name=backend.repo_name,
478 464 source_ref_type="rev", source_ref=commit0.raw_id,
479 465 target_ref_type="rev", target_ref=commit1.raw_id,
480 466 params=dict(merge='1')
481 467 ),
482 468 extra_environ=xhr_header, )
483 469
484 470 # outgoing commits between those commits
485 471 compare_page = ComparePage(response)
486 472 compare_page.contains_commits(commits=[commit1])
487 473
488 474 def test_errors_when_comparing_unknown_source_repo(self, backend):
489 475 repo = backend.repo
490 476
491 477 self.app.get(
492 478 route_path('repo_compare',
493 479 repo_name='badrepo',
494 480 source_ref_type="rev", source_ref='tip',
495 481 target_ref_type="rev", target_ref='tip',
496 482 params=dict(merge='1', target_repo=repo.repo_name)
497 483 ),
498 484 status=404)
499 485
500 486 def test_errors_when_comparing_unknown_target_repo(self, backend):
501 487 repo = backend.repo
502 488 badrepo = 'badrepo'
503 489
504 490 response = self.app.get(
505 491 route_path('repo_compare',
506 492 repo_name=repo.repo_name,
507 493 source_ref_type="rev", source_ref='tip',
508 494 target_ref_type="rev", target_ref='tip',
509 495 params=dict(merge='1', target_repo=badrepo),
510 496 ),
511 497 status=302)
512 498 redirected = response.follow()
513 499 redirected.mustcontain(
514 500 'Could not find the target repo: `{}`'.format(badrepo))
515 501
516 502 def test_compare_not_in_preview_mode(self, backend_stub):
517 503 commit0 = backend_stub.repo.get_commit(commit_idx=0)
518 504 commit1 = backend_stub.repo.get_commit(commit_idx=1)
519 505
520 506 response = self.app.get(
521 507 route_path('repo_compare',
522 508 repo_name=backend_stub.repo_name,
523 509 source_ref_type="rev", source_ref=commit0.raw_id,
524 510 target_ref_type="rev", target_ref=commit1.raw_id,
525 511 ))
526 512
527 513 # outgoing commits between those commits
528 514 compare_page = ComparePage(response)
529 515 compare_page.swap_is_visible()
530 516 compare_page.target_source_are_enabled()
531 517
532 518 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
533 519 orig = backend_hg.create_repo(number_of_commits=1)
534 520 fork = backend_hg.create_fork()
535 521
536 522 settings_util.create_repo_rhodecode_ui(
537 523 orig, 'extensions', value='', key='largefiles', active=False)
538 524 settings_util.create_repo_rhodecode_ui(
539 525 fork, 'extensions', value='', key='largefiles', active=True)
540 526
541 527 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
542 528 'MercurialRepository.compare')
543 529 with mock.patch(compare_module) as compare_mock:
544 530 compare_mock.side_effect = RepositoryRequirementError()
545 531
546 532 response = self.app.get(
547 533 route_path('repo_compare',
548 534 repo_name=orig.repo_name,
549 535 source_ref_type="rev", source_ref="tip",
550 536 target_ref_type="rev", target_ref="tip",
551 537 params=dict(merge='1', target_repo=fork.repo_name),
552 538 ),
553 539 status=302)
554 540
555 541 assert_session_flash(
556 542 response,
557 543 'Could not compare repos with different large file settings')
558 544
559 545
560 546 @pytest.mark.usefixtures("autologin_user")
561 547 class TestCompareControllerSvn(object):
562 548
563 549 def test_supports_references_with_path(self, app, backend_svn):
564 550 repo = backend_svn['svn-simple-layout']
565 551 commit_id = repo.get_commit(commit_idx=-1).raw_id
566 552 response = app.get(
567 553 route_path('repo_compare',
568 554 repo_name=repo.repo_name,
569 555 source_ref_type="tag",
570 556 source_ref="%s@%s" % ('tags/v0.1', commit_id),
571 557 target_ref_type="tag",
572 558 target_ref="%s@%s" % ('tags/v0.2', commit_id),
573 559 params=dict(merge='1'),
574 560 ),
575 561 status=200)
576 562
577 563 # Expecting no commits, since both paths are at the same revision
578 564 response.mustcontain('No commits in this compare')
579 565
580 566 # Should find only one file changed when comparing those two tags
581 567 response.mustcontain('example.py')
582 568 compare_page = ComparePage(response)
583 569 compare_page.contains_change_summary(1, 5, 1)
584 570
585 571 def test_shows_commits_if_different_ids(self, app, backend_svn):
586 572 repo = backend_svn['svn-simple-layout']
587 573 source_id = repo.get_commit(commit_idx=-6).raw_id
588 574 target_id = repo.get_commit(commit_idx=-1).raw_id
589 575 response = app.get(
590 576 route_path('repo_compare',
591 577 repo_name=repo.repo_name,
592 578 source_ref_type="tag",
593 579 source_ref="%s@%s" % ('tags/v0.1', source_id),
594 580 target_ref_type="tag",
595 581 target_ref="%s@%s" % ('tags/v0.2', target_id),
596 582 params=dict(merge='1')
597 583 ),
598 584 status=200)
599 585
600 586 # It should show commits
601 587 assert 'No commits in this compare' not in response.text
602 588
603 589 # Should find only one file changed when comparing those two tags
604 590 response.mustcontain('example.py')
605 591 compare_page = ComparePage(response)
606 592 compare_page.contains_change_summary(1, 5, 1)
607 593
608 594
609 595 class ComparePage(AssertResponse):
610 596 """
611 597 Abstracts the page template from the tests
612 598 """
613 599
614 600 def contains_file_links_and_anchors(self, files):
615 601 doc = lxml.html.fromstring(self.response.body)
616 602 for filename, file_id in files:
617 603 self.contains_one_anchor(file_id)
618 604 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
619 605 assert len(diffblock) == 2
620 606 for lnk in diffblock[0].cssselect('a'):
621 607 if 'permalink' in lnk.text:
622 608 assert '#{}'.format(file_id) in lnk.attrib['href']
623 609 break
624 610 else:
625 611 pytest.fail('Unable to find permalink')
626 612
627 613 def contains_change_summary(self, files_changed, inserted, deleted):
628 614 template = (
629 615 '{files_changed} file{plural} changed: '
630 616 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
631 617 self.response.mustcontain(template.format(
632 618 files_changed=files_changed,
633 619 plural="s" if files_changed > 1 else "",
634 620 inserted=inserted,
635 621 deleted=deleted))
636 622
637 623 def contains_commits(self, commits, ancestors=None):
638 624 response = self.response
639 625
640 626 for commit in commits:
641 627 # Expecting to see the commit message in an element which
642 628 # has the ID "c-{commit.raw_id}"
643 629 self.element_contains('#c-' + commit.raw_id, commit.message)
644 630 self.contains_one_link(
645 631 'r%s:%s' % (commit.idx, commit.short_id),
646 632 self._commit_url(commit))
647 633
648 634 if ancestors:
649 635 response.mustcontain('Ancestor')
650 636 for ancestor in ancestors:
651 637 self.contains_one_link(
652 638 ancestor.short_id, self._commit_url(ancestor))
653 639
654 640 def _commit_url(self, commit):
655 641 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
656 642
657 643 def swap_is_hidden(self):
658 644 assert '<a id="btn-swap"' not in self.response.text
659 645
660 646 def swap_is_visible(self):
661 647 assert '<a id="btn-swap"' in self.response.text
662 648
663 649 def target_source_are_disabled(self):
664 650 response = self.response
665 651 response.mustcontain("var enable_fields = false;")
666 652 response.mustcontain('.select2("enable", enable_fields)')
667 653
668 654 def target_source_are_enabled(self):
669 655 response = self.response
670 656 response.mustcontain("var enable_fields = true;")
@@ -1,168 +1,154 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from .test_repo_compare import ComparePage
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
29
30 base_url = {
31 'repo_compare_select': '/{repo_name}/compare',
32 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
33 }[name].format(**kwargs)
34
35 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
23 from rhodecode.tests.routes import route_path
38 24
39 25
40 26 @pytest.mark.usefixtures("autologin_user", "app")
41 27 class TestCompareView(object):
42 28
43 29 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
44 30 def test_compare_tag(self, backend):
45 31 tag1 = 'v0.1.2'
46 32 tag2 = 'v0.1.3'
47 33 response = self.app.get(
48 34 route_path(
49 35 'repo_compare',
50 36 repo_name=backend.repo_name,
51 37 source_ref_type="tag", source_ref=tag1,
52 38 target_ref_type="tag", target_ref=tag2),
53 39 status=200)
54 40
55 41 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
56 42 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
57 43
58 44 # outgoing commits between tags
59 45 commit_indexes = {
60 46 'git': [113] + list(range(115, 121)),
61 47 'hg': [112] + list(range(115, 121)),
62 48 }
63 49 repo = backend.repo
64 50 commits = (repo.get_commit(commit_idx=idx)
65 51 for idx in commit_indexes[backend.alias])
66 52 compare_page = ComparePage(response)
67 53 compare_page.contains_change_summary(11, 94, 64)
68 54 compare_page.contains_commits(commits)
69 55
70 56 # files diff
71 57 short_id = short_id_new = ''
72 58 if backend.alias == 'git':
73 59 short_id = '5a3a8fb00555'
74 60 short_id_new = '0ba5f8a46600'
75 61 if backend.alias == 'hg':
76 62 short_id = '17544fbfcd33'
77 63 short_id_new = 'a7e60bff65d5'
78 64
79 65 compare_page.contains_file_links_and_anchors([
80 66 # modified
81 67 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
82 68 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
83 69 # added
84 70 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
85 71 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
86 72 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
87 73 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
88 74 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
89 75 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
90 76 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
91 77 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
92 78 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
93 79 ])
94 80
95 81 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
96 82 def test_compare_tag_branch(self, backend):
97 83 revisions = {
98 84 'hg': {
99 85 'tag': 'v0.2.0',
100 86 'branch': 'default',
101 87 'response': (147, 5701, 10177)
102 88 },
103 89 'git': {
104 90 'tag': 'v0.2.2',
105 91 'branch': 'master',
106 92 'response': (70, 1855, 3002)
107 93 },
108 94 }
109 95
110 96 # Backend specific data, depends on the test repository for
111 97 # functional tests.
112 98 data = revisions[backend.alias]
113 99
114 100 response = self.app.get(
115 101 route_path(
116 102 'repo_compare',
117 103 repo_name=backend.repo_name,
118 104 source_ref_type='branch', source_ref=data['branch'],
119 105 target_ref_type="tag", target_ref=data['tag'],
120 106 ))
121 107
122 108 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
123 109 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
124 110 compare_page = ComparePage(response)
125 111 compare_page.contains_change_summary(*data['response'])
126 112
127 113 def test_index_branch(self, backend):
128 114 head_id = backend.default_head_id
129 115 response = self.app.get(
130 116 route_path(
131 117 'repo_compare',
132 118 repo_name=backend.repo_name,
133 119 source_ref_type="branch", source_ref=head_id,
134 120 target_ref_type="branch", target_ref=head_id,
135 121 ))
136 122
137 123 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
138 124
139 125 # branches are equal
140 126 response.mustcontain('No files')
141 127 response.mustcontain('No commits in this compare')
142 128
143 129 def test_compare_commits(self, backend):
144 130 repo = backend.repo
145 131 commit1 = repo.get_commit(commit_idx=0)
146 132 commit1_short_id = commit1.short_id
147 133 commit2 = repo.get_commit(commit_idx=1)
148 134 commit2_short_id = commit2.short_id
149 135
150 136 response = self.app.get(
151 137 route_path(
152 138 'repo_compare',
153 139 repo_name=backend.repo_name,
154 140 source_ref_type="rev", source_ref=commit1.raw_id,
155 141 target_ref_type="rev", target_ref=commit2.raw_id,
156 142 ))
157 143 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
158 144 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
159 145 compare_page = ComparePage(response)
160 146
161 147 # files
162 148 compare_page.contains_change_summary(1, 7, 0)
163 149
164 150 # outgoing commits between those commits
165 151 compare_page.contains_commits([commit2])
166 152 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
167 153 response.mustcontain(anchor)
168 154 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,292 +1,279 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 23 from rhodecode.lib.vcs import nodes
24 24 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.tests.utils import commit_change
27
28 fixture = Fixture()
27 from rhodecode.tests.routes import route_path
29 28
30 29
31 def route_path(name, params=None, **kwargs):
32 import urllib.request
33 import urllib.parse
34 import urllib.error
35
36 base_url = {
37 'repo_compare_select': '/{repo_name}/compare',
38 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
30 fixture = Fixture()
44 31
45 32
46 33 @pytest.mark.usefixtures("autologin_user", "app")
47 34 class TestSideBySideDiff(object):
48 35
49 36 def test_diff_sidebyside_single_commit(self, app, backend):
50 37 commit_id_range = {
51 38 'hg': {
52 39 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
53 40 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
54 41 'changes': (21, 943, 288),
55 42 },
56 43 'git': {
57 44 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
58 45 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
59 46 'changes': (20, 941, 286),
60 47 },
61 48
62 49 'svn': {
63 50 'commits': ['336',
64 51 '337'],
65 52 'changes': (21, 943, 288),
66 53 },
67 54 }
68 55
69 56 commit_info = commit_id_range[backend.alias]
70 57 commit2, commit1 = commit_info['commits']
71 58 file_changes = commit_info['changes']
72 59
73 60 response = self.app.get(route_path(
74 61 'repo_compare',
75 62 repo_name=backend.repo_name,
76 63 source_ref_type='rev',
77 64 source_ref=commit2,
78 65 target_repo=backend.repo_name,
79 66 target_ref_type='rev',
80 67 target_ref=commit1,
81 68 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
82 69 ))
83 70
84 71 compare_page = ComparePage(response)
85 72 compare_page.contains_change_summary(*file_changes)
86 73 response.mustcontain('Collapse 1 commit')
87 74
88 75 def test_diff_sidebyside_two_commits(self, app, backend):
89 76 commit_id_range = {
90 77 'hg': {
91 78 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
92 79 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
93 80 'changes': (32, 1165, 308),
94 81 },
95 82 'git': {
96 83 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
97 84 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
98 85 'changes': (31, 1163, 306),
99 86 },
100 87
101 88 'svn': {
102 89 'commits': ['335',
103 90 '337'],
104 91 'changes': (32, 1179, 310),
105 92 },
106 93 }
107 94
108 95 commit_info = commit_id_range[backend.alias]
109 96 commit2, commit1 = commit_info['commits']
110 97 file_changes = commit_info['changes']
111 98
112 99 response = self.app.get(route_path(
113 100 'repo_compare',
114 101 repo_name=backend.repo_name,
115 102 source_ref_type='rev',
116 103 source_ref=commit2,
117 104 target_repo=backend.repo_name,
118 105 target_ref_type='rev',
119 106 target_ref=commit1,
120 107 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
121 108 ))
122 109
123 110 compare_page = ComparePage(response)
124 111 compare_page.contains_change_summary(*file_changes)
125 112
126 113 response.mustcontain('Collapse 2 commits')
127 114
128 115 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
129 116 commit_id_range = {
130 117
131 118 'svn': {
132 119 'commits': ['330',
133 120 '337'],
134 121
135 122 },
136 123 }
137 124
138 125 commit_info = commit_id_range['svn']
139 126 commit2, commit1 = commit_info['commits']
140 127
141 128 response = self.app.get(route_path(
142 129 'repo_compare',
143 130 repo_name=backend_svn.repo_name,
144 131 source_ref_type='rev',
145 132 source_ref=commit2,
146 133 target_repo=backend_svn.repo_name,
147 134 target_ref_type='rev',
148 135 target_ref=commit1,
149 136 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
150 137 ))
151 138
152 139 response.mustcontain('Expand 7 commits')
153 140
154 141 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
155 142 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
156 143 f_path = b'test_sidebyside_file.py'
157 144 commit1_content = b'content-25d7e49c18b159446c\n'
158 145 commit2_content = b'content-603d6c72c46d953420\n'
159 146 repo = backend.create_repo()
160 147
161 148 commit1 = commit_change(
162 149 repo.repo_name, filename=f_path, content=commit1_content,
163 150 message='A', vcs_type=backend.alias, parent=None, newfile=True)
164 151
165 152 commit2 = commit_change(
166 153 repo.repo_name, filename=f_path, content=commit2_content,
167 154 message='B, child of A', vcs_type=backend.alias, parent=commit1)
168 155
169 156 response = self.app.get(route_path(
170 157 'repo_compare',
171 158 repo_name=repo.repo_name,
172 159 source_ref_type='rev',
173 160 source_ref=EmptyCommit().raw_id,
174 161 target_ref_type='rev',
175 162 target_ref=commit2.raw_id,
176 163 params=dict(diffmode='sidebyside')
177 164 ))
178 165
179 166 response.mustcontain('Collapse 2 commits')
180 167 response.mustcontain('123 file changed')
181 168
182 169 response.mustcontain(
183 170 'r%s:%s...r%s:%s' % (
184 171 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
185 172
186 173 response.mustcontain(f_path)
187 174
188 175 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
189 176 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
190 177 f_path = b'test_sidebyside_file.py'
191 178 commit1_content = b'content-25d7e49c18b159446c\n'
192 179 commit2_content = b'content-603d6c72c46d953420\n'
193 180 repo = backend.create_repo()
194 181
195 182 commit1 = commit_change(
196 183 repo.repo_name, filename=f_path, content=commit1_content,
197 184 message='A', vcs_type=backend.alias, parent=None, newfile=True)
198 185
199 186 commit2 = commit_change(
200 187 repo.repo_name, filename=f_path, content=commit2_content,
201 188 message='B, child of A', vcs_type=backend.alias, parent=commit1)
202 189
203 190 response = self.app.get(route_path(
204 191 'repo_compare',
205 192 repo_name=repo.repo_name,
206 193 source_ref_type='rev',
207 194 source_ref=EmptyCommit().raw_id,
208 195 target_ref_type='rev',
209 196 target_ref=commit2.raw_id,
210 197 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
211 198 ))
212 199
213 200 response.mustcontain('Collapse 2 commits')
214 201 response.mustcontain('1 file changed')
215 202
216 203 response.mustcontain(
217 204 'r%s:%s...r%s:%s' % (
218 205 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
219 206
220 207 response.mustcontain(f_path)
221 208
222 209 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
223 210 commits = [
224 211 {'message': 'First commit'},
225 212 {'message': 'Second commit'},
226 213 {'message': 'Commit with binary',
227 214 'added': [nodes.FileNode(b'file.empty', content=b'')]},
228 215 ]
229 216 f_path = 'file.empty'
230 217 repo = backend.create_repo(commits=commits)
231 218 commit1 = repo.get_commit(commit_idx=0)
232 219 commit2 = repo.get_commit(commit_idx=1)
233 220 commit3 = repo.get_commit(commit_idx=2)
234 221
235 222 response = self.app.get(route_path(
236 223 'repo_compare',
237 224 repo_name=repo.repo_name,
238 225 source_ref_type='rev',
239 226 source_ref=commit1.raw_id,
240 227 target_ref_type='rev',
241 228 target_ref=commit3.raw_id,
242 229 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
243 230 ))
244 231
245 232 response.mustcontain('Collapse 2 commits')
246 233 response.mustcontain('1 file changed')
247 234
248 235 response.mustcontain(
249 236 'r%s:%s...r%s:%s' % (
250 237 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
251 238
252 239 response.mustcontain(f_path)
253 240
254 241 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
255 242 commit_id_range = {
256 243 'hg': {
257 244 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
258 245 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
259 246 'changes': (1, 3, 3)
260 247 },
261 248 'git': {
262 249 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
263 250 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
264 251 'changes': (1, 3, 3)
265 252 },
266 253
267 254 'svn': {
268 255 'commits': ['335',
269 256 '337'],
270 257 'changes': (1, 3, 3)
271 258 },
272 259 }
273 260 f_path = 'docs/conf.py'
274 261
275 262 commit_info = commit_id_range[backend.alias]
276 263 commit2, commit1 = commit_info['commits']
277 264 file_changes = commit_info['changes']
278 265
279 266 response = self.app.get(route_path(
280 267 'repo_compare',
281 268 repo_name=backend.repo_name,
282 269 source_ref_type='rev',
283 270 source_ref=commit2,
284 271 target_ref_type='rev',
285 272 target_ref=commit1,
286 273 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
287 274 ))
288 275
289 276 response.mustcontain('Collapse 2 commits')
290 277
291 278 compare_page = ComparePage(response)
292 279 compare_page.contains_change_summary(*file_changes)
@@ -1,138 +1,122 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21 from rhodecode.model.auth_token import AuthTokenModel
22 22 from rhodecode.tests import TestController
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
29
30 base_url = {
31 'rss_feed_home': '/{repo_name}/feed-rss',
32 'atom_feed_home': '/{repo_name}/feed-atom',
33 'rss_feed_home_old': '/{repo_name}/feed/rss',
34 'atom_feed_home_old': '/{repo_name}/feed/atom',
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
23 from rhodecode.tests.routes import route_path
40 24
41 25
42 26 class TestFeedView(TestController):
43 27
44 28 @pytest.mark.parametrize("feed_type,response_types,content_type",[
45 29 ('rss', ['<rss version="2.0"'],
46 30 "application/rss+xml"),
47 31 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
48 32 "application/atom+xml"),
49 33 ])
50 34 def test_feed(self, backend, feed_type, response_types, content_type):
51 35 self.log_user()
52 36 response = self.app.get(
53 37 route_path('{}_feed_home'.format(feed_type),
54 38 repo_name=backend.repo_name))
55 39
56 40 for content in response_types:
57 41 response.mustcontain(content)
58 42
59 43 assert response.content_type == content_type
60 44
61 45 @pytest.mark.parametrize("feed_type, content_type", [
62 46 ('rss', "application/rss+xml"),
63 47 ('atom', "application/atom+xml")
64 48 ])
65 49 def test_feed_with_auth_token(
66 50 self, backend, user_admin, feed_type, content_type):
67 51 auth_token = user_admin.feed_token
68 52 assert auth_token != ''
69 53
70 54 response = self.app.get(
71 55 route_path(
72 56 '{}_feed_home'.format(feed_type),
73 57 repo_name=backend.repo_name,
74 58 params=dict(auth_token=auth_token)),
75 59 status=200)
76 60
77 61 assert response.content_type == content_type
78 62
79 63 @pytest.mark.parametrize("feed_type, content_type", [
80 64 ('rss', "application/rss+xml"),
81 65 ('atom', "application/atom+xml")
82 66 ])
83 67 def test_feed_with_auth_token_by_uid(
84 68 self, backend, user_admin, feed_type, content_type):
85 69 auth_token = user_admin.feed_token
86 70 assert auth_token != ''
87 71
88 72 response = self.app.get(
89 73 route_path(
90 74 '{}_feed_home'.format(feed_type),
91 75 repo_name='_{}'.format(backend.repo.repo_id),
92 76 params=dict(auth_token=auth_token)),
93 77 status=200)
94 78
95 79 assert response.content_type == content_type
96 80
97 81 @pytest.mark.parametrize("feed_type, content_type", [
98 82 ('rss', "application/rss+xml"),
99 83 ('atom', "application/atom+xml")
100 84 ])
101 85 def test_feed_old_urls_with_auth_token(
102 86 self, backend, user_admin, feed_type, content_type):
103 87 auth_token = user_admin.feed_token
104 88 assert auth_token != ''
105 89
106 90 response = self.app.get(
107 91 route_path(
108 92 '{}_feed_home_old'.format(feed_type),
109 93 repo_name=backend.repo_name,
110 94 params=dict(auth_token=auth_token)),
111 95 status=200)
112 96
113 97 assert response.content_type == content_type
114 98
115 99 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
116 100 def test_feed_with_auth_token_of_wrong_type(
117 101 self, backend, user_util, feed_type):
118 102 user = user_util.create_user()
119 103 auth_token = AuthTokenModel().create(
120 104 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
121 105 auth_token = auth_token.api_key
122 106
123 107 self.app.get(
124 108 route_path(
125 109 '{}_feed_home'.format(feed_type),
126 110 repo_name=backend.repo_name,
127 111 params=dict(auth_token=auth_token)),
128 112 status=302)
129 113
130 114 auth_token = AuthTokenModel().create(
131 115 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
132 116 auth_token = auth_token.api_key
133 117 self.app.get(
134 118 route_path(
135 119 '{}_feed_home'.format(feed_type),
136 120 repo_name=backend.repo_name,
137 121 params=dict(auth_token=auth_token)),
138 122 status=200)
@@ -1,1125 +1,1090 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21
22 22 import mock
23 23 import pytest
24 24 from collections import OrderedDict
25 25
26 26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 27 from rhodecode.apps.repository.views.repo_files import RepoFilesView, get_archive_name, get_path_sha
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.str_utils import safe_str
31 31 from rhodecode.lib.vcs import nodes
32 32 from rhodecode.lib.vcs.conf import settings
33 33 from rhodecode.model.db import Session, Repository
34 34
35 35 from rhodecode.tests import assert_session_flash
36 36 from rhodecode.tests.fixture import Fixture
37 from rhodecode.tests.routes import route_path
38
37 39
38 40 fixture = Fixture()
39 41
40 42
41 43 def get_node_history(backend_type):
42 44 return {
43 45 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 46 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 47 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 48 }[backend_type]
47 49
48 50
49 def route_path(name, params=None, **kwargs):
50 import urllib.request
51 import urllib.parse
52 import urllib.error
53
54 base_url = {
55 'repo_summary': '/{repo_name}',
56 'repo_archivefile': '/{repo_name}/archive/{fname}',
57 'repo_files_diff': '/{repo_name}/diff/{f_path}',
58 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
59 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
60 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
61 'repo_files:default_commit': '/{repo_name}/files',
62 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
63 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
64 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
65 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
66 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
67 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
68 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
69 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
70 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
71 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
72 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
73 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
74 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
75 'repo_files_upload_file': '/{repo_name}/upload_file/{commit_id}/{f_path}',
76 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
77 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
78 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
79 }[name].format(**kwargs)
80
81 if params:
82 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
83 return base_url
84
85
86 51 def assert_files_in_response(response, files, params):
87 52 template = (
88 53 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
89 54 _assert_items_in_response(response, files, template, params)
90 55
91 56
92 57 def assert_dirs_in_response(response, dirs, params):
93 58 template = (
94 59 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
95 60 _assert_items_in_response(response, dirs, template, params)
96 61
97 62
98 63 def _assert_items_in_response(response, items, template, params):
99 64 for item in items:
100 65 item_params = {'name': item}
101 66 item_params.update(params)
102 67 response.mustcontain(template % item_params)
103 68
104 69
105 70 def assert_timeago_in_response(response, items, params):
106 71 for item in items:
107 72 response.mustcontain(h.age_component(params['date']))
108 73
109 74
110 75 @pytest.mark.usefixtures("app")
111 76 class TestFilesViews(object):
112 77
113 78 def test_show_files(self, backend):
114 79 response = self.app.get(
115 80 route_path('repo_files',
116 81 repo_name=backend.repo_name,
117 82 commit_id='tip', f_path='/'))
118 83 commit = backend.repo.get_commit()
119 84
120 85 params = {
121 86 'repo_name': backend.repo_name,
122 87 'commit_id': commit.raw_id,
123 88 'date': commit.date
124 89 }
125 90 assert_dirs_in_response(response, ['docs', 'vcs'], params)
126 91 files = [
127 92 '.gitignore',
128 93 '.hgignore',
129 94 '.hgtags',
130 95 # TODO: missing in Git
131 96 # '.travis.yml',
132 97 'MANIFEST.in',
133 98 'README.rst',
134 99 # TODO: File is missing in svn repository
135 100 # 'run_test_and_report.sh',
136 101 'setup.cfg',
137 102 'setup.py',
138 103 'test_and_report.sh',
139 104 'tox.ini',
140 105 ]
141 106 assert_files_in_response(response, files, params)
142 107 assert_timeago_in_response(response, files, params)
143 108
144 109 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
145 110 repo = backend_hg['subrepos']
146 111 response = self.app.get(
147 112 route_path('repo_files',
148 113 repo_name=repo.repo_name,
149 114 commit_id='tip', f_path='/'))
150 115 assert_response = response.assert_response()
151 116 assert_response.contains_one_link(
152 117 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
153 118
154 119 def test_show_files_links_submodules_with_absolute_url_subpaths(
155 120 self, backend_hg):
156 121 repo = backend_hg['subrepos']
157 122 response = self.app.get(
158 123 route_path('repo_files',
159 124 repo_name=repo.repo_name,
160 125 commit_id='tip', f_path='/'))
161 126 assert_response = response.assert_response()
162 127 assert_response.contains_one_link(
163 128 'subpaths-path @ 000000000000',
164 129 'http://sub-base.example.com/subpaths-path')
165 130
166 131 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
167 132 def test_files_menu(self, backend):
168 133 new_branch = "temp_branch_name"
169 134 commits = [
170 135 {'message': 'a'},
171 136 {'message': 'b', 'branch': new_branch}
172 137 ]
173 138 backend.create_repo(commits)
174 139 backend.repo.landing_rev = f"branch:{new_branch}"
175 140 Session().commit()
176 141
177 142 # get response based on tip and not new commit
178 143 response = self.app.get(
179 144 route_path('repo_files',
180 145 repo_name=backend.repo_name,
181 146 commit_id='tip', f_path='/'))
182 147
183 148 # make sure Files menu url is not tip but new commit
184 149 landing_rev = backend.repo.landing_ref_name
185 150 files_url = route_path('repo_files:default_path',
186 151 repo_name=backend.repo_name,
187 152 commit_id=landing_rev, params={'at': landing_rev})
188 153
189 154 assert landing_rev != 'tip'
190 155 response.mustcontain(f'<li class="active"><a class="menulink" href="{files_url}">')
191 156
192 157 def test_show_files_commit(self, backend):
193 158 commit = backend.repo.get_commit(commit_idx=32)
194 159
195 160 response = self.app.get(
196 161 route_path('repo_files',
197 162 repo_name=backend.repo_name,
198 163 commit_id=commit.raw_id, f_path='/'))
199 164
200 165 dirs = ['docs', 'tests']
201 166 files = ['README.rst']
202 167 params = {
203 168 'repo_name': backend.repo_name,
204 169 'commit_id': commit.raw_id,
205 170 }
206 171 assert_dirs_in_response(response, dirs, params)
207 172 assert_files_in_response(response, files, params)
208 173
209 174 def test_show_files_different_branch(self, backend):
210 175 branches = dict(
211 176 hg=(150, ['git']),
212 177 # TODO: Git test repository does not contain other branches
213 178 git=(633, ['master']),
214 179 # TODO: Branch support in Subversion
215 180 svn=(150, [])
216 181 )
217 182 idx, branches = branches[backend.alias]
218 183 commit = backend.repo.get_commit(commit_idx=idx)
219 184 response = self.app.get(
220 185 route_path('repo_files',
221 186 repo_name=backend.repo_name,
222 187 commit_id=commit.raw_id, f_path='/'))
223 188
224 189 assert_response = response.assert_response()
225 190 for branch in branches:
226 191 assert_response.element_contains('.tags .branchtag', branch)
227 192
228 193 def test_show_files_paging(self, backend):
229 194 repo = backend.repo
230 195 indexes = [73, 92, 109, 1, 0]
231 196 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
232 197 for rev in indexes]
233 198
234 199 for idx in idx_map:
235 200 response = self.app.get(
236 201 route_path('repo_files',
237 202 repo_name=backend.repo_name,
238 203 commit_id=idx[1], f_path='/'))
239 204
240 205 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
241 206
242 207 def test_file_source(self, backend):
243 208 commit = backend.repo.get_commit(commit_idx=167)
244 209 response = self.app.get(
245 210 route_path('repo_files',
246 211 repo_name=backend.repo_name,
247 212 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
248 213
249 214 msgbox = """<div class="commit">%s</div>"""
250 215 response.mustcontain(msgbox % (commit.message, ))
251 216
252 217 assert_response = response.assert_response()
253 218 if commit.branch:
254 219 assert_response.element_contains(
255 220 '.tags.tags-main .branchtag', commit.branch)
256 221 if commit.tags:
257 222 for tag in commit.tags:
258 223 assert_response.element_contains('.tags.tags-main .tagtag', tag)
259 224
260 225 def test_file_source_annotated(self, backend):
261 226 response = self.app.get(
262 227 route_path('repo_files:annotated',
263 228 repo_name=backend.repo_name,
264 229 commit_id='tip', f_path='vcs/nodes.py'))
265 230 expected_commits = {
266 231 'hg': 'r356',
267 232 'git': 'r345',
268 233 'svn': 'r208',
269 234 }
270 235 response.mustcontain(expected_commits[backend.alias])
271 236
272 237 def test_file_source_authors(self, backend):
273 238 response = self.app.get(
274 239 route_path('repo_file_authors',
275 240 repo_name=backend.repo_name,
276 241 commit_id='tip', f_path='vcs/nodes.py'))
277 242 expected_authors = {
278 243 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
279 244 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
280 245 'svn': ('marcin', 'lukasz'),
281 246 }
282 247
283 248 for author in expected_authors[backend.alias]:
284 249 response.mustcontain(author)
285 250
286 251 def test_file_source_authors_with_annotation(self, backend):
287 252 response = self.app.get(
288 253 route_path('repo_file_authors',
289 254 repo_name=backend.repo_name,
290 255 commit_id='tip', f_path='vcs/nodes.py',
291 256 params=dict(annotate=1)))
292 257 expected_authors = {
293 258 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
294 259 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
295 260 'svn': ('marcin', 'lukasz'),
296 261 }
297 262
298 263 for author in expected_authors[backend.alias]:
299 264 response.mustcontain(author)
300 265
301 266 def test_file_source_history(self, backend, xhr_header):
302 267 response = self.app.get(
303 268 route_path('repo_file_history',
304 269 repo_name=backend.repo_name,
305 270 commit_id='tip', f_path='vcs/nodes.py'),
306 271 extra_environ=xhr_header)
307 272 assert get_node_history(backend.alias) == json.loads(response.body)
308 273
309 274 def test_file_source_history_svn(self, backend_svn, xhr_header):
310 275 simple_repo = backend_svn['svn-simple-layout']
311 276 response = self.app.get(
312 277 route_path('repo_file_history',
313 278 repo_name=simple_repo.repo_name,
314 279 commit_id='tip', f_path='trunk/example.py'),
315 280 extra_environ=xhr_header)
316 281
317 282 expected_data = json.loads(
318 283 fixture.load_resource('svn_node_history_branches.json'))
319 284
320 285 assert expected_data == response.json
321 286
322 287 def test_file_source_history_with_annotation(self, backend, xhr_header):
323 288 response = self.app.get(
324 289 route_path('repo_file_history',
325 290 repo_name=backend.repo_name,
326 291 commit_id='tip', f_path='vcs/nodes.py',
327 292 params=dict(annotate=1)),
328 293
329 294 extra_environ=xhr_header)
330 295 assert get_node_history(backend.alias) == json.loads(response.body)
331 296
332 297 def test_tree_search_top_level(self, backend, xhr_header):
333 298 commit = backend.repo.get_commit(commit_idx=173)
334 299 response = self.app.get(
335 300 route_path('repo_files_nodelist',
336 301 repo_name=backend.repo_name,
337 302 commit_id=commit.raw_id, f_path='/'),
338 303 extra_environ=xhr_header)
339 304 assert 'nodes' in response.json
340 305 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
341 306
342 307 def test_tree_search_missing_xhr(self, backend):
343 308 self.app.get(
344 309 route_path('repo_files_nodelist',
345 310 repo_name=backend.repo_name,
346 311 commit_id='tip', f_path='/'),
347 312 status=404)
348 313
349 314 def test_tree_search_at_path(self, backend, xhr_header):
350 315 commit = backend.repo.get_commit(commit_idx=173)
351 316 response = self.app.get(
352 317 route_path('repo_files_nodelist',
353 318 repo_name=backend.repo_name,
354 319 commit_id=commit.raw_id, f_path='/docs'),
355 320 extra_environ=xhr_header)
356 321 assert 'nodes' in response.json
357 322 nodes = response.json['nodes']
358 323 assert {'name': 'docs/api', 'type': 'dir'} in nodes
359 324 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
360 325
361 326 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
362 327 commit = backend.repo.get_commit(commit_idx=173)
363 328 response = self.app.get(
364 329 route_path('repo_files_nodelist',
365 330 repo_name=backend.repo_name,
366 331 commit_id=commit.raw_id, f_path='/docs/api'),
367 332 extra_environ=xhr_header)
368 333 assert 'nodes' in response.json
369 334 nodes = response.json['nodes']
370 335 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
371 336
372 337 def test_tree_search_at_path_missing_xhr(self, backend):
373 338 self.app.get(
374 339 route_path('repo_files_nodelist',
375 340 repo_name=backend.repo_name,
376 341 commit_id='tip', f_path='/docs'),
377 342 status=404)
378 343
379 344 def test_nodetree(self, backend, xhr_header):
380 345 commit = backend.repo.get_commit(commit_idx=173)
381 346 response = self.app.get(
382 347 route_path('repo_nodetree_full',
383 348 repo_name=backend.repo_name,
384 349 commit_id=commit.raw_id, f_path='/'),
385 350 extra_environ=xhr_header)
386 351
387 352 assert_response = response.assert_response()
388 353
389 354 for attr in ['data-commit-id', 'data-date', 'data-author']:
390 355 elements = assert_response.get_elements('[{}]'.format(attr))
391 356 assert len(elements) > 1
392 357
393 358 for element in elements:
394 359 assert element.get(attr)
395 360
396 361 def test_nodetree_if_file(self, backend, xhr_header):
397 362 commit = backend.repo.get_commit(commit_idx=173)
398 363 response = self.app.get(
399 364 route_path('repo_nodetree_full',
400 365 repo_name=backend.repo_name,
401 366 commit_id=commit.raw_id, f_path='README.rst'),
402 367 extra_environ=xhr_header)
403 368 assert response.text == ''
404 369
405 370 def test_nodetree_wrong_path(self, backend, xhr_header):
406 371 commit = backend.repo.get_commit(commit_idx=173)
407 372 response = self.app.get(
408 373 route_path('repo_nodetree_full',
409 374 repo_name=backend.repo_name,
410 375 commit_id=commit.raw_id, f_path='/dont-exist'),
411 376 extra_environ=xhr_header)
412 377
413 378 err = 'error: There is no file nor ' \
414 379 'directory at the given path'
415 380 assert err in response.text
416 381
417 382 def test_nodetree_missing_xhr(self, backend):
418 383 self.app.get(
419 384 route_path('repo_nodetree_full',
420 385 repo_name=backend.repo_name,
421 386 commit_id='tip', f_path='/'),
422 387 status=404)
423 388
424 389
425 390 @pytest.mark.usefixtures("app", "autologin_user")
426 391 class TestRawFileHandling(object):
427 392
428 393 def test_download_file(self, backend):
429 394 commit = backend.repo.get_commit(commit_idx=173)
430 395 response = self.app.get(
431 396 route_path('repo_file_download',
432 397 repo_name=backend.repo_name,
433 398 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
434 399
435 400 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
436 401 assert response.content_type == "text/x-python"
437 402
438 403 def test_download_file_wrong_cs(self, backend):
439 404 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
440 405
441 406 response = self.app.get(
442 407 route_path('repo_file_download',
443 408 repo_name=backend.repo_name,
444 409 commit_id=raw_id, f_path='vcs/nodes.svg'),
445 410 status=404)
446 411
447 412 msg = """No such commit exists for this repository"""
448 413 response.mustcontain(msg)
449 414
450 415 def test_download_file_wrong_f_path(self, backend):
451 416 commit = backend.repo.get_commit(commit_idx=173)
452 417 f_path = 'vcs/ERRORnodes.py'
453 418
454 419 response = self.app.get(
455 420 route_path('repo_file_download',
456 421 repo_name=backend.repo_name,
457 422 commit_id=commit.raw_id, f_path=f_path),
458 423 status=404)
459 424
460 425 msg = (
461 426 "There is no file nor directory at the given path: "
462 427 "`%s` at commit %s" % (f_path, commit.short_id))
463 428 response.mustcontain(msg)
464 429
465 430 def test_file_raw(self, backend):
466 431 commit = backend.repo.get_commit(commit_idx=173)
467 432 response = self.app.get(
468 433 route_path('repo_file_raw',
469 434 repo_name=backend.repo_name,
470 435 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
471 436
472 437 assert response.content_type == "text/plain"
473 438
474 439 def test_file_raw_binary(self, backend):
475 440 commit = backend.repo.get_commit()
476 441 response = self.app.get(
477 442 route_path('repo_file_raw',
478 443 repo_name=backend.repo_name,
479 444 commit_id=commit.raw_id,
480 445 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
481 446
482 447 assert response.content_disposition == 'inline'
483 448
484 449 def test_raw_file_wrong_cs(self, backend):
485 450 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
486 451
487 452 response = self.app.get(
488 453 route_path('repo_file_raw',
489 454 repo_name=backend.repo_name,
490 455 commit_id=raw_id, f_path='vcs/nodes.svg'),
491 456 status=404)
492 457
493 458 msg = """No such commit exists for this repository"""
494 459 response.mustcontain(msg)
495 460
496 461 def test_raw_wrong_f_path(self, backend):
497 462 commit = backend.repo.get_commit(commit_idx=173)
498 463 f_path = 'vcs/ERRORnodes.py'
499 464 response = self.app.get(
500 465 route_path('repo_file_raw',
501 466 repo_name=backend.repo_name,
502 467 commit_id=commit.raw_id, f_path=f_path),
503 468 status=404)
504 469
505 470 msg = (
506 471 "There is no file nor directory at the given path: "
507 472 "`%s` at commit %s" % (f_path, commit.short_id))
508 473 response.mustcontain(msg)
509 474
510 475 def test_raw_svg_should_not_be_rendered(self, backend):
511 476 backend.create_repo()
512 477 backend.ensure_file(b"xss.svg")
513 478 response = self.app.get(
514 479 route_path('repo_file_raw',
515 480 repo_name=backend.repo_name,
516 481 commit_id='tip', f_path='xss.svg'),)
517 482 # If the content type is image/svg+xml then it allows to render HTML
518 483 # and malicious SVG.
519 484 assert response.content_type == "text/plain"
520 485
521 486
522 487 @pytest.mark.usefixtures("app")
523 488 class TestRepositoryArchival(object):
524 489
525 490 def test_archival(self, backend):
526 491 backend.enable_downloads()
527 492 commit = backend.repo.get_commit(commit_idx=173)
528 493
529 494 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
530 495 path_sha = get_path_sha('/')
531 496 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
532 497
533 498 fname = commit.raw_id + extension
534 499 response = self.app.get(
535 500 route_path('repo_archivefile',
536 501 repo_name=backend.repo_name,
537 502 fname=fname))
538 503
539 504 assert response.status == '200 OK'
540 505 headers = [
541 506 ('Content-Disposition', f'attachment; filename={filename}'),
542 507 ('Content-Type', content_type),
543 508 ]
544 509
545 510 for header in headers:
546 511 assert header in list(response.headers.items())
547 512
548 513 def test_archival_no_hash(self, backend):
549 514 backend.enable_downloads()
550 515 commit = backend.repo.get_commit(commit_idx=173)
551 516 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
552 517 path_sha = get_path_sha('/')
553 518 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha, with_hash=False)
554 519
555 520 fname = commit.raw_id + extension
556 521 response = self.app.get(
557 522 route_path('repo_archivefile',
558 523 repo_name=backend.repo_name,
559 524 fname=fname, params={'with_hash': 0}))
560 525
561 526 assert response.status == '200 OK'
562 527 headers = [
563 528 ('Content-Disposition', f'attachment; filename={filename}'),
564 529 ('Content-Type', content_type),
565 530 ]
566 531
567 532 for header in headers:
568 533 assert header in list(response.headers.items())
569 534
570 535 def test_archival_at_path(self, backend):
571 536 backend.enable_downloads()
572 537 commit = backend.repo.get_commit(commit_idx=190)
573 538 at_path = 'vcs'
574 539
575 540 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
576 541 path_sha = get_path_sha(at_path)
577 542 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
578 543
579 544 fname = commit.raw_id + extension
580 545 response = self.app.get(
581 546 route_path('repo_archivefile',
582 547 repo_name=backend.repo_name,
583 548 fname=fname, params={'at_path': at_path}))
584 549
585 550 assert response.status == '200 OK'
586 551 headers = [
587 552 ('Content-Disposition', f'attachment; filename={filename}'),
588 553 ('Content-Type', content_type),
589 554 ]
590 555
591 556 for header in headers:
592 557 assert header in list(response.headers.items())
593 558
594 559 @pytest.mark.parametrize('arch_ext',[
595 560 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
596 561 def test_archival_wrong_ext(self, backend, arch_ext):
597 562 backend.enable_downloads()
598 563 commit = backend.repo.get_commit(commit_idx=173)
599 564
600 565 fname = commit.raw_id + '.' + arch_ext
601 566
602 567 response = self.app.get(
603 568 route_path('repo_archivefile',
604 569 repo_name=backend.repo_name,
605 570 fname=fname))
606 571 response.mustcontain(
607 572 'Unknown archive type for: `{}`'.format(fname))
608 573
609 574 @pytest.mark.parametrize('commit_id', [
610 575 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
611 576 def test_archival_wrong_commit_id(self, backend, commit_id):
612 577 backend.enable_downloads()
613 578 fname = f'{commit_id}.zip'
614 579
615 580 response = self.app.get(
616 581 route_path('repo_archivefile',
617 582 repo_name=backend.repo_name,
618 583 fname=fname))
619 584 response.mustcontain('Unknown commit_id')
620 585
621 586
622 587 @pytest.mark.usefixtures("app")
623 588 class TestFilesDiff(object):
624 589
625 590 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
626 591 def test_file_full_diff(self, backend, diff):
627 592 commit1 = backend.repo.get_commit(commit_idx=-1)
628 593 commit2 = backend.repo.get_commit(commit_idx=-2)
629 594
630 595 response = self.app.get(
631 596 route_path('repo_files_diff',
632 597 repo_name=backend.repo_name,
633 598 f_path='README'),
634 599 params={
635 600 'diff1': commit2.raw_id,
636 601 'diff2': commit1.raw_id,
637 602 'fulldiff': '1',
638 603 'diff': diff,
639 604 })
640 605
641 606 if diff == 'diff':
642 607 # use redirect since this is OLD view redirecting to compare page
643 608 response = response.follow()
644 609
645 610 # It's a symlink to README.rst
646 611 response.mustcontain('README.rst')
647 612 response.mustcontain('No newline at end of file')
648 613
649 614 def test_file_binary_diff(self, backend):
650 615 commits = [
651 616 {'message': 'First commit'},
652 617 {'message': 'Commit with binary',
653 618 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]},
654 619 ]
655 620 repo = backend.create_repo(commits=commits)
656 621
657 622 response = self.app.get(
658 623 route_path('repo_files_diff',
659 624 repo_name=backend.repo_name,
660 625 f_path='file.bin'),
661 626 params={
662 627 'diff1': repo.get_commit(commit_idx=0).raw_id,
663 628 'diff2': repo.get_commit(commit_idx=1).raw_id,
664 629 'fulldiff': '1',
665 630 'diff': 'diff',
666 631 })
667 632 # use redirect since this is OLD view redirecting to compare page
668 633 response = response.follow()
669 634 response.mustcontain('Collapse 1 commit')
670 635 file_changes = (1, 0, 0)
671 636
672 637 compare_page = ComparePage(response)
673 638 compare_page.contains_change_summary(*file_changes)
674 639
675 640 if backend.alias == 'svn':
676 641 response.mustcontain('new file 10644')
677 642 # TODO(marcink): SVN doesn't yet detect binary changes
678 643 else:
679 644 response.mustcontain('new file 100644')
680 645 response.mustcontain('binary diff hidden')
681 646
682 647 def test_diff_2way(self, backend):
683 648 commit1 = backend.repo.get_commit(commit_idx=-1)
684 649 commit2 = backend.repo.get_commit(commit_idx=-2)
685 650 response = self.app.get(
686 651 route_path('repo_files_diff_2way_redirect',
687 652 repo_name=backend.repo_name,
688 653 f_path='README'),
689 654 params={
690 655 'diff1': commit2.raw_id,
691 656 'diff2': commit1.raw_id,
692 657 })
693 658 # use redirect since this is OLD view redirecting to compare page
694 659 response = response.follow()
695 660
696 661 # It's a symlink to README.rst
697 662 response.mustcontain('README.rst')
698 663 response.mustcontain('No newline at end of file')
699 664
700 665 def test_requires_one_commit_id(self, backend, autologin_user):
701 666 response = self.app.get(
702 667 route_path('repo_files_diff',
703 668 repo_name=backend.repo_name,
704 669 f_path='README.rst'),
705 670 status=400)
706 671 response.mustcontain(
707 672 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
708 673
709 674 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
710 675 repo = vcsbackend.repo
711 676 response = self.app.get(
712 677 route_path('repo_files_diff',
713 678 repo_name=repo.name,
714 679 f_path='does-not-exist-in-any-commit'),
715 680 params={
716 681 'diff1': repo[0].raw_id,
717 682 'diff2': repo[1].raw_id
718 683 })
719 684
720 685 response = response.follow()
721 686 response.mustcontain('No files')
722 687
723 688 def test_returns_redirect_if_file_not_changed(self, backend):
724 689 commit = backend.repo.get_commit(commit_idx=-1)
725 690 response = self.app.get(
726 691 route_path('repo_files_diff_2way_redirect',
727 692 repo_name=backend.repo_name,
728 693 f_path='README'),
729 694 params={
730 695 'diff1': commit.raw_id,
731 696 'diff2': commit.raw_id,
732 697 })
733 698
734 699 response = response.follow()
735 700 response.mustcontain('No files')
736 701 response.mustcontain('No commits in this compare')
737 702
738 703 def test_supports_diff_to_different_path_svn(self, backend_svn):
739 704 #TODO: check this case
740 705 return
741 706
742 707 repo = backend_svn['svn-simple-layout'].scm_instance()
743 708 commit_id_1 = '24'
744 709 commit_id_2 = '26'
745 710
746 711 response = self.app.get(
747 712 route_path('repo_files_diff',
748 713 repo_name=backend_svn.repo_name,
749 714 f_path='trunk/example.py'),
750 715 params={
751 716 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
752 717 'diff2': commit_id_2,
753 718 })
754 719
755 720 response = response.follow()
756 721 response.mustcontain(
757 722 # diff contains this
758 723 "Will print out a useful message on invocation.")
759 724
760 725 # Note: Expecting that we indicate the user what's being compared
761 726 response.mustcontain("trunk/example.py")
762 727 response.mustcontain("tags/v0.2/example.py")
763 728
764 729 def test_show_rev_redirects_to_svn_path(self, backend_svn):
765 730 #TODO: check this case
766 731 return
767 732
768 733 repo = backend_svn['svn-simple-layout'].scm_instance()
769 734 commit_id = repo[-1].raw_id
770 735
771 736 response = self.app.get(
772 737 route_path('repo_files_diff',
773 738 repo_name=backend_svn.repo_name,
774 739 f_path='trunk/example.py'),
775 740 params={
776 741 'diff1': 'branches/argparse/example.py@' + commit_id,
777 742 'diff2': commit_id,
778 743 },
779 744 status=302)
780 745 response = response.follow()
781 746 assert response.headers['Location'].endswith(
782 747 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
783 748
784 749 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
785 750 #TODO: check this case
786 751 return
787 752
788 753 repo = backend_svn['svn-simple-layout'].scm_instance()
789 754 commit_id = repo[-1].raw_id
790 755 response = self.app.get(
791 756 route_path('repo_files_diff',
792 757 repo_name=backend_svn.repo_name,
793 758 f_path='trunk/example.py'),
794 759 params={
795 760 'diff1': 'branches/argparse/example.py@' + commit_id,
796 761 'diff2': commit_id,
797 762 'show_rev': 'Show at Revision',
798 763 'annotate': 'true',
799 764 },
800 765 status=302)
801 766 response = response.follow()
802 767 assert response.headers['Location'].endswith(
803 768 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
804 769
805 770
806 771 @pytest.mark.usefixtures("app", "autologin_user")
807 772 class TestModifyFilesWithWebInterface(object):
808 773
809 774 def test_add_file_view(self, backend):
810 775 self.app.get(
811 776 route_path('repo_files_add_file',
812 777 repo_name=backend.repo_name,
813 778 commit_id='tip', f_path='/')
814 779 )
815 780
816 781 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
817 782 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
818 783 backend.create_repo()
819 784 filename = 'init.py'
820 785 response = self.app.post(
821 786 route_path('repo_files_create_file',
822 787 repo_name=backend.repo_name,
823 788 commit_id='tip', f_path='/'),
824 789 params={
825 790 'content': "",
826 791 'filename': filename,
827 792 'csrf_token': csrf_token,
828 793 },
829 794 status=302)
830 795 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
831 796 assert_session_flash(response, expected_msg)
832 797
833 798 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
834 799 commit_id = backend.repo.get_commit().raw_id
835 800 response = self.app.post(
836 801 route_path('repo_files_create_file',
837 802 repo_name=backend.repo_name,
838 803 commit_id=commit_id, f_path='/'),
839 804 params={
840 805 'content': "foo",
841 806 'csrf_token': csrf_token,
842 807 },
843 808 status=302)
844 809
845 810 assert_session_flash(response, 'No filename specified')
846 811
847 812 def test_add_file_into_repo_errors_and_no_commits(
848 813 self, backend, csrf_token):
849 814 repo = backend.create_repo()
850 815 # Create a file with no filename, it will display an error but
851 816 # the repo has no commits yet
852 817 response = self.app.post(
853 818 route_path('repo_files_create_file',
854 819 repo_name=repo.repo_name,
855 820 commit_id='tip', f_path='/'),
856 821 params={
857 822 'content': "foo",
858 823 'csrf_token': csrf_token,
859 824 },
860 825 status=302)
861 826
862 827 assert_session_flash(response, 'No filename specified')
863 828
864 829 # Not allowed, redirect to the summary
865 830 redirected = response.follow()
866 831 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
867 832
868 833 # As there are no commits, displays the summary page with the error of
869 834 # creating a file with no filename
870 835
871 836 assert redirected.request.path == summary_url
872 837
873 838 @pytest.mark.parametrize("filename, clean_filename", [
874 839 ('/abs/foo', 'abs/foo'),
875 840 ('../rel/foo', 'rel/foo'),
876 841 ('file/../foo/foo', 'file/foo/foo'),
877 842 ])
878 843 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
879 844 repo = backend.create_repo()
880 845 commit_id = repo.get_commit().raw_id
881 846
882 847 response = self.app.post(
883 848 route_path('repo_files_create_file',
884 849 repo_name=repo.repo_name,
885 850 commit_id=commit_id, f_path='/'),
886 851 params={
887 852 'content': "foo",
888 853 'filename': filename,
889 854 'csrf_token': csrf_token,
890 855 },
891 856 status=302)
892 857
893 858 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
894 859 assert_session_flash(response, expected_msg)
895 860
896 861 @pytest.mark.parametrize("cnt, filename, content", [
897 862 (1, 'foo.txt', "Content"),
898 863 (2, 'dir/foo.rst', "Content"),
899 864 (3, 'dir/foo-second.rst', "Content"),
900 865 (4, 'rel/dir/foo.bar', "Content"),
901 866 ])
902 867 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
903 868 repo = backend.create_repo()
904 869 commit_id = repo.get_commit().raw_id
905 870 response = self.app.post(
906 871 route_path('repo_files_create_file',
907 872 repo_name=repo.repo_name,
908 873 commit_id=commit_id, f_path='/'),
909 874 params={
910 875 'content': content,
911 876 'filename': filename,
912 877 'csrf_token': csrf_token,
913 878 },
914 879 status=302)
915 880
916 881 expected_msg = 'Successfully committed new file `{}`'.format(filename)
917 882 assert_session_flash(response, expected_msg)
918 883
919 884 def test_edit_file_view(self, backend):
920 885 response = self.app.get(
921 886 route_path('repo_files_edit_file',
922 887 repo_name=backend.repo_name,
923 888 commit_id=backend.default_head_id,
924 889 f_path='vcs/nodes.py'),
925 890 status=200)
926 891 response.mustcontain("Module holding everything related to vcs nodes.")
927 892
928 893 def test_edit_file_view_not_on_branch(self, backend):
929 894 repo = backend.create_repo()
930 895 backend.ensure_file(b"vcs/nodes.py")
931 896
932 897 response = self.app.get(
933 898 route_path('repo_files_edit_file',
934 899 repo_name=repo.repo_name,
935 900 commit_id='tip',
936 901 f_path='vcs/nodes.py'),
937 902 status=302)
938 903 assert_session_flash(
939 904 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
940 905
941 906 def test_edit_file_view_commit_changes(self, backend, csrf_token):
942 907 repo = backend.create_repo()
943 908 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
944 909
945 910 response = self.app.post(
946 911 route_path('repo_files_update_file',
947 912 repo_name=repo.repo_name,
948 913 commit_id=backend.default_head_id,
949 914 f_path='vcs/nodes.py'),
950 915 params={
951 916 'content': "print 'hello world'",
952 917 'message': 'I committed',
953 918 'filename': "vcs/nodes.py",
954 919 'csrf_token': csrf_token,
955 920 },
956 921 status=302)
957 922 assert_session_flash(
958 923 response, 'Successfully committed changes to file `vcs/nodes.py`')
959 924 tip = repo.get_commit(commit_idx=-1)
960 925 assert tip.message == 'I committed'
961 926
962 927 def test_edit_file_view_commit_changes_default_message(self, backend,
963 928 csrf_token):
964 929 repo = backend.create_repo()
965 930 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
966 931
967 932 commit_id = (
968 933 backend.default_branch_name or
969 934 backend.repo.scm_instance().commit_ids[-1])
970 935
971 936 response = self.app.post(
972 937 route_path('repo_files_update_file',
973 938 repo_name=repo.repo_name,
974 939 commit_id=commit_id,
975 940 f_path='vcs/nodes.py'),
976 941 params={
977 942 'content': "print 'hello world'",
978 943 'message': '',
979 944 'filename': "vcs/nodes.py",
980 945 'csrf_token': csrf_token,
981 946 },
982 947 status=302)
983 948 assert_session_flash(
984 949 response, 'Successfully committed changes to file `vcs/nodes.py`')
985 950 tip = repo.get_commit(commit_idx=-1)
986 951 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
987 952
988 953 def test_delete_file_view(self, backend):
989 954 self.app.get(
990 955 route_path('repo_files_remove_file',
991 956 repo_name=backend.repo_name,
992 957 commit_id=backend.default_head_id,
993 958 f_path='vcs/nodes.py'),
994 959 status=200)
995 960
996 961 def test_delete_file_view_not_on_branch(self, backend):
997 962 repo = backend.create_repo()
998 963 backend.ensure_file(b'vcs/nodes.py')
999 964
1000 965 response = self.app.get(
1001 966 route_path('repo_files_remove_file',
1002 967 repo_name=repo.repo_name,
1003 968 commit_id='tip',
1004 969 f_path='vcs/nodes.py'),
1005 970 status=302)
1006 971 assert_session_flash(
1007 972 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
1008 973
1009 974 def test_delete_file_view_commit_changes(self, backend, csrf_token):
1010 975 repo = backend.create_repo()
1011 976 backend.ensure_file(b"vcs/nodes.py")
1012 977
1013 978 response = self.app.post(
1014 979 route_path('repo_files_delete_file',
1015 980 repo_name=repo.repo_name,
1016 981 commit_id=backend.default_head_id,
1017 982 f_path='vcs/nodes.py'),
1018 983 params={
1019 984 'message': 'i committed',
1020 985 'csrf_token': csrf_token,
1021 986 },
1022 987 status=302)
1023 988 assert_session_flash(
1024 989 response, 'Successfully deleted file `vcs/nodes.py`')
1025 990
1026 991
1027 992 @pytest.mark.usefixtures("app")
1028 993 class TestFilesViewOtherCases(object):
1029 994
1030 995 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1031 996 self, backend_stub, autologin_regular_user, user_regular,
1032 997 user_util):
1033 998
1034 999 repo = backend_stub.create_repo()
1035 1000 user_util.grant_user_permission_to_repo(
1036 1001 repo, user_regular, 'repository.write')
1037 1002 response = self.app.get(
1038 1003 route_path('repo_files',
1039 1004 repo_name=repo.repo_name,
1040 1005 commit_id='tip', f_path='/'))
1041 1006
1042 1007 repo_file_add_url = route_path(
1043 1008 'repo_files_add_file',
1044 1009 repo_name=repo.repo_name,
1045 1010 commit_id=0, f_path='')
1046 1011 add_new = f'<a class="alert-link" href="{repo_file_add_url}">add a new file</a>'
1047 1012
1048 1013 repo_file_upload_url = route_path(
1049 1014 'repo_files_upload_file',
1050 1015 repo_name=repo.repo_name,
1051 1016 commit_id=0, f_path='')
1052 1017 upload_new = f'<a class="alert-link" href="{repo_file_upload_url}">upload a new file</a>'
1053 1018
1054 1019 assert_session_flash(
1055 1020 response,
1056 1021 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
1057 1022 )
1058 1023
1059 1024 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1060 1025 self, backend_stub, autologin_regular_user):
1061 1026 repo = backend_stub.create_repo()
1062 1027 # init session for anon user
1063 1028 route_path('repo_summary', repo_name=repo.repo_name)
1064 1029
1065 1030 repo_file_add_url = route_path(
1066 1031 'repo_files_add_file',
1067 1032 repo_name=repo.repo_name,
1068 1033 commit_id=0, f_path='')
1069 1034
1070 1035 response = self.app.get(
1071 1036 route_path('repo_files',
1072 1037 repo_name=repo.repo_name,
1073 1038 commit_id='tip', f_path='/'))
1074 1039
1075 1040 assert_session_flash(response, no_=repo_file_add_url)
1076 1041
1077 1042 @pytest.mark.parametrize('file_node', [
1078 1043 b'archive/file.zip',
1079 1044 b'diff/my-file.txt',
1080 1045 b'render.py',
1081 1046 b'render',
1082 1047 b'remove_file',
1083 1048 b'remove_file/to-delete.txt',
1084 1049 ])
1085 1050 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1086 1051 backend.create_repo()
1087 1052 backend.ensure_file(file_node)
1088 1053
1089 1054 self.app.get(
1090 1055 route_path('repo_files',
1091 1056 repo_name=backend.repo_name,
1092 1057 commit_id='tip', f_path=safe_str(file_node)),
1093 1058 status=200)
1094 1059
1095 1060
1096 1061 class TestAdjustFilePathForSvn(object):
1097 1062 """
1098 1063 SVN specific adjustments of node history in RepoFilesView.
1099 1064 """
1100 1065
1101 1066 def test_returns_path_relative_to_matched_reference(self):
1102 1067 repo = self._repo(branches=['trunk'])
1103 1068 self.assert_file_adjustment('trunk/file', 'file', repo)
1104 1069
1105 1070 def test_does_not_modify_file_if_no_reference_matches(self):
1106 1071 repo = self._repo(branches=['trunk'])
1107 1072 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1108 1073
1109 1074 def test_does_not_adjust_partial_directory_names(self):
1110 1075 repo = self._repo(branches=['trun'])
1111 1076 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1112 1077
1113 1078 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1114 1079 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1115 1080 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1116 1081
1117 1082 def assert_file_adjustment(self, f_path, expected, repo):
1118 1083 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1119 1084 assert result == expected
1120 1085
1121 1086 def _repo(self, branches=None):
1122 1087 repo = mock.Mock()
1123 1088 repo.branches = OrderedDict((name, '0') for name in branches or [])
1124 1089 repo.tags = {}
1125 1090 return repo
@@ -1,334 +1,317 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
23 23
24 24 from rhodecode.tests.fixture import Fixture
25 25 from rhodecode.lib import helpers as h
26 26
27 27 from rhodecode.model.db import Repository
28 28 from rhodecode.model.repo import RepoModel
29 29 from rhodecode.model.user import UserModel
30 30 from rhodecode.model.meta import Session
31
32 fixture = Fixture()
31 from rhodecode.tests.routes import route_path
33 32
34 33
35 def route_path(name, params=None, **kwargs):
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39
40 base_url = {
41 'repo_summary': '/{repo_name}',
42 'repo_creating_check': '/{repo_name}/repo_creating_check',
43 'repo_fork_new': '/{repo_name}/fork',
44 'repo_fork_create': '/{repo_name}/fork/create',
45 'repo_forks_show_all': '/{repo_name}/forks',
46 'repo_forks_data': '/{repo_name}/forks/data',
47 }[name].format(**kwargs)
48
49 if params:
50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
51 return base_url
34 fixture = Fixture()
52 35
53 36
54 37 FORK_NAME = {
55 38 'hg': HG_FORK,
56 39 'git': GIT_FORK
57 40 }
58 41
59 42
60 43 @pytest.mark.skip_backends('svn')
61 44 class TestRepoForkViewTests(TestController):
62 45
63 46 def test_show_forks(self, backend, xhr_header):
64 47 self.log_user()
65 48 response = self.app.get(
66 49 route_path('repo_forks_data', repo_name=backend.repo_name),
67 50 extra_environ=xhr_header)
68 51
69 52 assert response.json == {u'data': [], u'draw': None,
70 53 u'recordsFiltered': 0, u'recordsTotal': 0}
71 54
72 55 def test_no_permissions_to_fork_page(self, backend, user_util):
73 56 user = user_util.create_user(password='qweqwe')
74 57 user_id = user.user_id
75 58 self.log_user(user.username, 'qweqwe')
76 59
77 60 user_model = UserModel()
78 61 user_model.revoke_perm(user_id, 'hg.fork.repository')
79 62 user_model.grant_perm(user_id, 'hg.fork.none')
80 63 u = UserModel().get(user_id)
81 64 u.inherit_default_permissions = False
82 65 Session().commit()
83 66 # try create a fork
84 67 self.app.get(
85 68 route_path('repo_fork_new', repo_name=backend.repo_name),
86 69 status=404)
87 70
88 71 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
89 72 user = user_util.create_user(password='qweqwe')
90 73 user_id = user.user_id
91 74 self.log_user(user.username, 'qweqwe')
92 75
93 76 user_model = UserModel()
94 77 user_model.revoke_perm(user_id, 'hg.fork.repository')
95 78 user_model.grant_perm(user_id, 'hg.fork.none')
96 79 u = UserModel().get(user_id)
97 80 u.inherit_default_permissions = False
98 81 Session().commit()
99 82 # try create a fork
100 83 self.app.post(
101 84 route_path('repo_fork_create', repo_name=backend.repo_name),
102 85 {'csrf_token': csrf_token},
103 86 status=404)
104 87
105 88 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
106 89 # try create a fork
107 90 response = self.app.post(
108 91 route_path('repo_fork_create', repo_name=backend.repo_name),
109 92 {'csrf_token': csrf_token},
110 93 status=200)
111 94 # test if html fill works fine
112 95 response.mustcontain('Missing value')
113 96
114 97 def test_create_fork_page(self, autologin_user, backend):
115 98 self.app.get(
116 99 route_path('repo_fork_new', repo_name=backend.repo_name),
117 100 status=200)
118 101
119 102 def test_create_and_show_fork(
120 103 self, autologin_user, backend, csrf_token, xhr_header):
121 104
122 105 # create a fork
123 106 fork_name = FORK_NAME[backend.alias]
124 107 description = 'fork of vcs test'
125 108 repo_name = backend.repo_name
126 109 source_repo = Repository.get_by_repo_name(repo_name)
127 110 creation_args = {
128 111 'repo_name': fork_name,
129 112 'repo_group': '',
130 113 'fork_parent_id': source_repo.repo_id,
131 114 'repo_type': backend.alias,
132 115 'description': description,
133 116 'private': 'False',
134 117 'csrf_token': csrf_token,
135 118 }
136 119
137 120 self.app.post(
138 121 route_path('repo_fork_create', repo_name=repo_name), creation_args)
139 122
140 123 response = self.app.get(
141 124 route_path('repo_forks_data', repo_name=repo_name),
142 125 extra_environ=xhr_header)
143 126
144 127 assert response.json['data'][0]['fork_name'] == \
145 128 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
146 129
147 130 # remove this fork
148 131 fixture.destroy_repo(fork_name)
149 132
150 133 def test_fork_create(self, autologin_user, backend, csrf_token):
151 134 fork_name = FORK_NAME[backend.alias]
152 135 description = 'fork of vcs test'
153 136 repo_name = backend.repo_name
154 137 source_repo = Repository.get_by_repo_name(repo_name)
155 138 creation_args = {
156 139 'repo_name': fork_name,
157 140 'repo_group': '',
158 141 'fork_parent_id': source_repo.repo_id,
159 142 'repo_type': backend.alias,
160 143 'description': description,
161 144 'private': 'False',
162 145 'csrf_token': csrf_token,
163 146 }
164 147 self.app.post(
165 148 route_path('repo_fork_create', repo_name=repo_name), creation_args)
166 149 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
167 150 assert repo.fork.repo_name == backend.repo_name
168 151
169 152 # run the check page that triggers the flash message
170 153 response = self.app.get(
171 154 route_path('repo_creating_check', repo_name=fork_name))
172 155 # test if we have a message that fork is ok
173 156 assert_session_flash(response,
174 157 'Forked repository %s as <a href="/%s">%s</a>' % (
175 158 repo_name, fork_name, fork_name))
176 159
177 160 # test if the fork was created in the database
178 161 fork_repo = Session().query(Repository)\
179 162 .filter(Repository.repo_name == fork_name).one()
180 163
181 164 assert fork_repo.repo_name == fork_name
182 165 assert fork_repo.fork.repo_name == repo_name
183 166
184 167 # test if the repository is visible in the list ?
185 168 response = self.app.get(
186 169 h.route_path('repo_summary', repo_name=fork_name))
187 170 response.mustcontain(fork_name)
188 171 response.mustcontain(backend.alias)
189 172 response.mustcontain('Fork of')
190 173 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
191 174
192 175 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
193 176 group = fixture.create_repo_group('vc')
194 177 group_id = group.group_id
195 178 fork_name = FORK_NAME[backend.alias]
196 179 fork_name_full = 'vc/%s' % fork_name
197 180 description = 'fork of vcs test'
198 181 repo_name = backend.repo_name
199 182 source_repo = Repository.get_by_repo_name(repo_name)
200 183 creation_args = {
201 184 'repo_name': fork_name,
202 185 'repo_group': group_id,
203 186 'fork_parent_id': source_repo.repo_id,
204 187 'repo_type': backend.alias,
205 188 'description': description,
206 189 'private': 'False',
207 190 'csrf_token': csrf_token,
208 191 }
209 192 self.app.post(
210 193 route_path('repo_fork_create', repo_name=repo_name), creation_args)
211 194 repo = Repository.get_by_repo_name(fork_name_full)
212 195 assert repo.fork.repo_name == backend.repo_name
213 196
214 197 # run the check page that triggers the flash message
215 198 response = self.app.get(
216 199 route_path('repo_creating_check', repo_name=fork_name_full))
217 200 # test if we have a message that fork is ok
218 201 assert_session_flash(response,
219 202 'Forked repository %s as <a href="/%s">%s</a>' % (
220 203 repo_name, fork_name_full, fork_name_full))
221 204
222 205 # test if the fork was created in the database
223 206 fork_repo = Session().query(Repository)\
224 207 .filter(Repository.repo_name == fork_name_full).one()
225 208
226 209 assert fork_repo.repo_name == fork_name_full
227 210 assert fork_repo.fork.repo_name == repo_name
228 211
229 212 # test if the repository is visible in the list ?
230 213 response = self.app.get(
231 214 h.route_path('repo_summary', repo_name=fork_name_full))
232 215 response.mustcontain(fork_name_full)
233 216 response.mustcontain(backend.alias)
234 217
235 218 response.mustcontain('Fork of')
236 219 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
237 220
238 221 fixture.destroy_repo(fork_name_full)
239 222 fixture.destroy_repo_group(group_id)
240 223
241 224 def test_fork_read_permission(self, backend, xhr_header, user_util):
242 225 user = user_util.create_user(password='qweqwe')
243 226 user_id = user.user_id
244 227 self.log_user(user.username, 'qweqwe')
245 228
246 229 # create a fake fork
247 230 fork = user_util.create_repo(repo_type=backend.alias)
248 231 source = user_util.create_repo(repo_type=backend.alias)
249 232 repo_name = source.repo_name
250 233
251 234 fork.fork_id = source.repo_id
252 235 fork_name = fork.repo_name
253 236 Session().commit()
254 237
255 238 forks = Repository.query()\
256 239 .filter(Repository.repo_type == backend.alias)\
257 240 .filter(Repository.fork_id == source.repo_id).all()
258 241 assert 1 == len(forks)
259 242
260 243 # set read permissions for this
261 244 RepoModel().grant_user_permission(
262 245 repo=forks[0], user=user_id, perm='repository.read')
263 246 Session().commit()
264 247
265 248 response = self.app.get(
266 249 route_path('repo_forks_data', repo_name=repo_name),
267 250 extra_environ=xhr_header)
268 251
269 252 assert response.json['data'][0]['fork_name'] == \
270 253 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
271 254
272 255 def test_fork_none_permission(self, backend, xhr_header, user_util):
273 256 user = user_util.create_user(password='qweqwe')
274 257 user_id = user.user_id
275 258 self.log_user(user.username, 'qweqwe')
276 259
277 260 # create a fake fork
278 261 fork = user_util.create_repo(repo_type=backend.alias)
279 262 source = user_util.create_repo(repo_type=backend.alias)
280 263 repo_name = source.repo_name
281 264
282 265 fork.fork_id = source.repo_id
283 266
284 267 Session().commit()
285 268
286 269 forks = Repository.query()\
287 270 .filter(Repository.repo_type == backend.alias)\
288 271 .filter(Repository.fork_id == source.repo_id).all()
289 272 assert 1 == len(forks)
290 273
291 274 # set none
292 275 RepoModel().grant_user_permission(
293 276 repo=forks[0], user=user_id, perm='repository.none')
294 277 Session().commit()
295 278
296 279 # fork shouldn't be there
297 280 response = self.app.get(
298 281 route_path('repo_forks_data', repo_name=repo_name),
299 282 extra_environ=xhr_header)
300 283
301 284 assert response.json == {u'data': [], u'draw': None,
302 285 u'recordsFiltered': 0, u'recordsTotal': 0}
303 286
304 287 @pytest.mark.parametrize('url_type', [
305 288 'repo_fork_new',
306 289 'repo_fork_create'
307 290 ])
308 291 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
309 292 user = user_util.create_user(password='qweqwe')
310 293 self.log_user(user.username, 'qweqwe')
311 294
312 295 # create a temporary repo
313 296 source = user_util.create_repo(repo_type=backend.alias)
314 297 repo_name = source.repo_name
315 298 repo = Repository.get_by_repo_name(repo_name)
316 299 repo.archived = True
317 300 Session().commit()
318 301
319 302 response = self.app.get(
320 303 route_path(url_type, repo_name=repo_name), status=302)
321 304
322 305 msg = 'Action not supported for archived repository.'
323 306 assert_session_flash(response, msg)
324 307
325 308
326 309 class TestSVNFork(TestController):
327 310 @pytest.mark.parametrize('route_name', [
328 311 'repo_fork_create', 'repo_fork_new'
329 312 ])
330 313 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
331 314
332 315 self.app.get(route_path(
333 316 route_name, repo_name=backend_svn.repo_name),
334 317 status=404)
@@ -1,150 +1,134 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.lib.hash_utils import md5_safe
23 23 from rhodecode.model.db import Repository
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
26
27
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
26 from rhodecode.tests.routes import route_path
32 27
33 base_url = {
34 'repo_summary': '/{repo_name}',
35 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
36 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
37 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
38 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
44 28
45 29
46 30 @pytest.mark.usefixtures("app")
47 31 class TestRepoIssueTracker(object):
48 32 def test_issuetracker_index(self, autologin_user, backend):
49 33 repo = backend.create_repo()
50 34 response = self.app.get(route_path('edit_repo_issuetracker',
51 35 repo_name=repo.repo_name))
52 36 assert response.status_code == 200
53 37
54 38 def test_add_and_test_issuetracker_patterns(
55 39 self, autologin_user, backend, csrf_token, request, xhr_header):
56 40 pattern = 'issuetracker_pat'
57 41 another_pattern = pattern+'1'
58 42 post_url = route_path(
59 43 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
60 44 post_data = {
61 45 'new_pattern_pattern_0': pattern,
62 46 'new_pattern_url_0': 'http://url',
63 47 'new_pattern_prefix_0': 'prefix',
64 48 'new_pattern_description_0': 'description',
65 49 'new_pattern_pattern_1': another_pattern,
66 50 'new_pattern_url_1': '/url1',
67 51 'new_pattern_prefix_1': 'prefix1',
68 52 'new_pattern_description_1': 'description1',
69 53 'csrf_token': csrf_token
70 54 }
71 55 self.app.post(post_url, post_data, status=302)
72 56 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
73 57 settings = self.settings_model.get_repo_settings()
74 58 self.uid = md5_safe(pattern)
75 59 assert settings[self.uid]['pat'] == pattern
76 60 self.another_uid = md5_safe(another_pattern)
77 61 assert settings[self.another_uid]['pat'] == another_pattern
78 62
79 63 # test pattern
80 64 data = {'test_text': 'example of issuetracker_pat replacement',
81 65 'csrf_token': csrf_token}
82 66 response = self.app.post(
83 67 route_path('edit_repo_issuetracker_test',
84 68 repo_name=backend.repo.repo_name),
85 69 extra_environ=xhr_header, params=data)
86 70
87 71 assert response.text == \
88 72 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
89 73
90 74 @request.addfinalizer
91 75 def cleanup():
92 76 self.settings_model.delete_entries(self.uid)
93 77 self.settings_model.delete_entries(self.another_uid)
94 78
95 79 def test_edit_issuetracker_pattern(
96 80 self, autologin_user, backend, csrf_token, request):
97 81 entry_key = 'issuetracker_pat_'
98 82 pattern = 'issuetracker_pat2'
99 83 old_pattern = 'issuetracker_pat'
100 84 old_uid = md5_safe(old_pattern)
101 85
102 86 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
103 87 entry_key+old_uid, old_pattern, 'unicode')
104 88 Session().add(sett)
105 89 Session().commit()
106 90 post_url = route_path(
107 91 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
108 92 post_data = {
109 93 'new_pattern_pattern_0': pattern,
110 94 'new_pattern_url_0': '/url',
111 95 'new_pattern_prefix_0': 'prefix',
112 96 'new_pattern_description_0': 'description',
113 97 'uid': old_uid,
114 98 'csrf_token': csrf_token
115 99 }
116 100 self.app.post(post_url, post_data, status=302)
117 101 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
118 102 settings = self.settings_model.get_repo_settings()
119 103 self.uid = md5_safe(pattern)
120 104 assert settings[self.uid]['pat'] == pattern
121 105 with pytest.raises(KeyError):
122 106 key = settings[old_uid]
123 107
124 108 @request.addfinalizer
125 109 def cleanup():
126 110 self.settings_model.delete_entries(self.uid)
127 111
128 112 def test_delete_issuetracker_pattern(
129 113 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
130 114 repo = backend.create_repo()
131 115 repo_name = repo.repo_name
132 116 entry_key = 'issuetracker_pat_'
133 117 pattern = 'issuetracker_pat3'
134 118 uid = md5_safe(pattern)
135 119 settings_util.create_repo_rhodecode_setting(
136 120 repo=backend.repo, name=entry_key+uid,
137 121 value=entry_key, type_='unicode', cleanup=False)
138 122
139 123 self.app.post(
140 124 route_path(
141 125 'edit_repo_issuetracker_delete',
142 126 repo_name=backend.repo.repo_name),
143 127 {
144 128 'uid': uid,
145 129 'csrf_token': csrf_token,
146 130 '': ''
147 131 }, extra_environ=xhr_header, status=200)
148 132 settings = IssueTrackerSettingsModel(
149 133 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
150 134 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,75 +1,55 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 import mock
21 20 import pytest
22 21
23 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 22 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 from rhodecode.model.meta import Session
27 from rhodecode.tests import (
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
23
29 24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
30 26
31 27 fixture = Fixture()
32 28
33 29
34 def route_path(name, params=None, **kwargs):
35 import urllib.request
36 import urllib.parse
37 import urllib.error
38
39 base_url = {
40 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
41 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
42
43 }[name].format(**kwargs)
44
45 if params:
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 return base_url
48
49
50 30 def _get_permission_for_user(user, repo):
51 31 perm = UserRepoToPerm.query()\
52 32 .filter(UserRepoToPerm.repository ==
53 33 Repository.get_by_repo_name(repo))\
54 34 .filter(UserRepoToPerm.user == User.get_by_username(user))\
55 35 .all()
56 36 return perm
57 37
58 38
59 39 @pytest.mark.usefixtures('autologin_user', 'app')
60 40 class TestAdminRepoMaintenance(object):
61 41 @pytest.mark.parametrize('urlname', [
62 42 'edit_repo_maintenance',
63 43 ])
64 44 def test_show_page(self, urlname, app, backend):
65 45 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
66 46
67 47 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
68 48 repo_name = backend_hg.repo_name
69 49
70 50 response = app.get(
71 51 route_path('edit_repo_maintenance_execute',
72 52 repo_name=repo_name,),
73 53 extra_environ=xhr_header)
74 54
75 55 assert "HG Verify repo" in ''.join(response.json)
@@ -1,78 +1,64 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests.utils import permission_update_data_generator
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
29
30 base_url = {
31 'edit_repo_perms': '/{repo_name}/settings/permissions'
32 # update is the same url
33 }[name].format(**kwargs)
34
35 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
23 from rhodecode.tests.routes import route_path
38 24
39 25
40 26 @pytest.mark.usefixtures("app")
41 27 class TestRepoPermissionsView(object):
42 28
43 29 def test_edit_perms_view(self, user_util, autologin_user):
44 30 repo = user_util.create_repo()
45 31 self.app.get(
46 32 route_path('edit_repo_perms',
47 33 repo_name=repo.repo_name), status=200)
48 34
49 35 def test_update_permissions(self, csrf_token, user_util):
50 36 repo = user_util.create_repo()
51 37 repo_name = repo.repo_name
52 38 user = user_util.create_user()
53 39 user_id = user.user_id
54 40 username = user.username
55 41
56 42 # grant new
57 43 form_data = permission_update_data_generator(
58 44 csrf_token,
59 45 default='repository.write',
60 46 grant=[(user_id, 'repository.write', username, 'user')])
61 47
62 48 response = self.app.post(
63 49 route_path('edit_repo_perms',
64 50 repo_name=repo_name), form_data).follow()
65 51
66 52 assert 'Repository access permissions updated' in response
67 53
68 54 # revoke given
69 55 form_data = permission_update_data_generator(
70 56 csrf_token,
71 57 default='repository.read',
72 58 revoke=[(user_id, 'user')])
73 59
74 60 response = self.app.post(
75 61 route_path('edit_repo_perms',
76 62 repo_name=repo_name), form_data).follow()
77 63
78 64 assert 'Repository access permissions updated' in response
@@ -1,1680 +1,1651 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19 import mock
20 20 import pytest
21 21
22 22 import rhodecode
23 23 from rhodecode.lib import helpers as h
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib.ext_json import json
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.model.comment import CommentsModel
34 34 from rhodecode.tests import (
35 35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36
37
38 def route_path(name, params=None, **kwargs):
39 import urllib.request
40 import urllib.parse
41 import urllib.error
42
43 base_url = {
44 'repo_changelog': '/{repo_name}/changelog',
45 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
46 'repo_commits': '/{repo_name}/commits',
47 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
48 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
49 'pullrequest_show_all': '/{repo_name}/pull-request',
50 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
51 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
52 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
53 'pullrequest_new': '/{repo_name}/pull-request/new',
54 'pullrequest_create': '/{repo_name}/pull-request/create',
55 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
56 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
57 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
58 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
59 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
60 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
61 }[name].format(**kwargs)
62
63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
65 return base_url
36 from rhodecode.tests.routes import route_path
66 37
67 38
68 39 @pytest.mark.usefixtures('app', 'autologin_user')
69 40 @pytest.mark.backends("git", "hg")
70 41 class TestPullrequestsView(object):
71 42
72 43 def test_index(self, backend):
73 44 self.app.get(route_path(
74 45 'pullrequest_new',
75 46 repo_name=backend.repo_name))
76 47
77 48 def test_option_menu_create_pull_request_exists(self, backend):
78 49 repo_name = backend.repo_name
79 50 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
80 51
81 52 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
82 53 'pullrequest_new', repo_name=repo_name)
83 54 response.mustcontain(create_pr_link)
84 55
85 56 def test_create_pr_form_with_raw_commit_id(self, backend):
86 57 repo = backend.repo
87 58
88 59 self.app.get(
89 60 route_path('pullrequest_new', repo_name=repo.repo_name,
90 61 commit=repo.get_commit().raw_id),
91 62 status=200)
92 63
93 64 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
94 65 @pytest.mark.parametrize('range_diff', ["0", "1"])
95 66 def test_show(self, pr_util, pr_merge_enabled, range_diff):
96 67 pull_request = pr_util.create_pull_request(
97 68 mergeable=pr_merge_enabled, enable_notifications=False)
98 69
99 70 response = self.app.get(route_path(
100 71 'pullrequest_show',
101 72 repo_name=pull_request.target_repo.scm_instance().name,
102 73 pull_request_id=pull_request.pull_request_id,
103 74 params={'range-diff': range_diff}))
104 75
105 76 for commit_id in pull_request.revisions:
106 77 response.mustcontain(commit_id)
107 78
108 79 response.mustcontain(pull_request.target_ref_parts.type)
109 80 response.mustcontain(pull_request.target_ref_parts.name)
110 81
111 82 response.mustcontain('class="pull-request-merge"')
112 83
113 84 if pr_merge_enabled:
114 85 response.mustcontain('Pull request reviewer approval is pending')
115 86 else:
116 87 response.mustcontain('Server-side pull request merging is disabled.')
117 88
118 89 if range_diff == "1":
119 90 response.mustcontain('Turn off: Show the diff as commit range')
120 91
121 92 def test_show_versions_of_pr(self, backend, csrf_token):
122 93 commits = [
123 94 {'message': 'initial-commit',
124 95 'added': [FileNode(b'test-file.txt', b'LINE1\n')]},
125 96
126 97 {'message': 'commit-1',
127 98 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\n')]},
128 99 # Above is the initial version of PR that changes a single line
129 100
130 101 # from now on we'll add 3x commit adding a nother line on each step
131 102 {'message': 'commit-2',
132 103 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\n')]},
133 104
134 105 {'message': 'commit-3',
135 106 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\nLINE4\n')]},
136 107
137 108 {'message': 'commit-4',
138 109 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
139 110 ]
140 111
141 112 commit_ids = backend.create_master_repo(commits)
142 113 target = backend.create_repo(heads=['initial-commit'])
143 114 source = backend.create_repo(heads=['commit-1'])
144 115 source_repo_name = source.repo_name
145 116 target_repo_name = target.repo_name
146 117
147 118 target_ref = 'branch:{branch}:{commit_id}'.format(
148 119 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
149 120 source_ref = 'branch:{branch}:{commit_id}'.format(
150 121 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
151 122
152 123 response = self.app.post(
153 124 route_path('pullrequest_create', repo_name=source.repo_name),
154 125 [
155 126 ('source_repo', source_repo_name),
156 127 ('source_ref', source_ref),
157 128 ('target_repo', target_repo_name),
158 129 ('target_ref', target_ref),
159 130 ('common_ancestor', commit_ids['initial-commit']),
160 131 ('pullrequest_title', 'Title'),
161 132 ('pullrequest_desc', 'Description'),
162 133 ('description_renderer', 'markdown'),
163 134 ('__start__', 'review_members:sequence'),
164 135 ('__start__', 'reviewer:mapping'),
165 136 ('user_id', '1'),
166 137 ('__start__', 'reasons:sequence'),
167 138 ('reason', 'Some reason'),
168 139 ('__end__', 'reasons:sequence'),
169 140 ('__start__', 'rules:sequence'),
170 141 ('__end__', 'rules:sequence'),
171 142 ('mandatory', 'False'),
172 143 ('__end__', 'reviewer:mapping'),
173 144 ('__end__', 'review_members:sequence'),
174 145 ('__start__', 'revisions:sequence'),
175 146 ('revisions', commit_ids['commit-1']),
176 147 ('__end__', 'revisions:sequence'),
177 148 ('user', ''),
178 149 ('csrf_token', csrf_token),
179 150 ],
180 151 status=302)
181 152
182 153 location = response.headers['Location']
183 154
184 155 pull_request_id = location.rsplit('/', 1)[1]
185 156 assert pull_request_id != 'new'
186 157 pull_request = PullRequest.get(int(pull_request_id))
187 158
188 159 pull_request_id = pull_request.pull_request_id
189 160
190 161 # Show initial version of PR
191 162 response = self.app.get(
192 163 route_path('pullrequest_show',
193 164 repo_name=target_repo_name,
194 165 pull_request_id=pull_request_id))
195 166
196 167 response.mustcontain('commit-1')
197 168 response.mustcontain(no=['commit-2'])
198 169 response.mustcontain(no=['commit-3'])
199 170 response.mustcontain(no=['commit-4'])
200 171
201 172 response.mustcontain('cb-addition"></span><span>LINE2</span>')
202 173 response.mustcontain(no=['LINE3'])
203 174 response.mustcontain(no=['LINE4'])
204 175 response.mustcontain(no=['LINE5'])
205 176
206 177 # update PR #1
207 178 source_repo = Repository.get_by_repo_name(source_repo_name)
208 179 backend.pull_heads(source_repo, heads=['commit-2'])
209 180 response = self.app.post(
210 181 route_path('pullrequest_update',
211 182 repo_name=target_repo_name, pull_request_id=pull_request_id),
212 183 params={'update_commits': 'true', 'csrf_token': csrf_token})
213 184
214 185 # update PR #2
215 186 source_repo = Repository.get_by_repo_name(source_repo_name)
216 187 backend.pull_heads(source_repo, heads=['commit-3'])
217 188 response = self.app.post(
218 189 route_path('pullrequest_update',
219 190 repo_name=target_repo_name, pull_request_id=pull_request_id),
220 191 params={'update_commits': 'true', 'csrf_token': csrf_token})
221 192
222 193 # update PR #3
223 194 source_repo = Repository.get_by_repo_name(source_repo_name)
224 195 backend.pull_heads(source_repo, heads=['commit-4'])
225 196 response = self.app.post(
226 197 route_path('pullrequest_update',
227 198 repo_name=target_repo_name, pull_request_id=pull_request_id),
228 199 params={'update_commits': 'true', 'csrf_token': csrf_token})
229 200
230 201 # Show final version !
231 202 response = self.app.get(
232 203 route_path('pullrequest_show',
233 204 repo_name=target_repo_name,
234 205 pull_request_id=pull_request_id))
235 206
236 207 # 3 updates, and the latest == 4
237 208 response.mustcontain('4 versions available for this pull request')
238 209 response.mustcontain(no=['rhodecode diff rendering error'])
239 210
240 211 # initial show must have 3 commits, and 3 adds
241 212 response.mustcontain('commit-1')
242 213 response.mustcontain('commit-2')
243 214 response.mustcontain('commit-3')
244 215 response.mustcontain('commit-4')
245 216
246 217 response.mustcontain('cb-addition"></span><span>LINE2</span>')
247 218 response.mustcontain('cb-addition"></span><span>LINE3</span>')
248 219 response.mustcontain('cb-addition"></span><span>LINE4</span>')
249 220 response.mustcontain('cb-addition"></span><span>LINE5</span>')
250 221
251 222 # fetch versions
252 223 pr = PullRequest.get(pull_request_id)
253 224 versions = [x.pull_request_version_id for x in pr.versions.all()]
254 225 assert len(versions) == 3
255 226
256 227 # show v1,v2,v3,v4
257 228 def cb_line(text):
258 229 return 'cb-addition"></span><span>{}</span>'.format(text)
259 230
260 231 def cb_context(text):
261 232 return '<span class="cb-code"><span class="cb-action cb-context">' \
262 233 '</span><span>{}</span></span>'.format(text)
263 234
264 235 commit_tests = {
265 236 # in response, not in response
266 237 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
267 238 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
268 239 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
269 240 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
270 241 }
271 242 diff_tests = {
272 243 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
273 244 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
274 245 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
275 246 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
276 247 }
277 248 for idx, ver in enumerate(versions, 1):
278 249
279 250 response = self.app.get(
280 251 route_path('pullrequest_show',
281 252 repo_name=target_repo_name,
282 253 pull_request_id=pull_request_id,
283 254 params={'version': ver}))
284 255
285 256 response.mustcontain(no=['rhodecode diff rendering error'])
286 257 response.mustcontain('Showing changes at v{}'.format(idx))
287 258
288 259 yes, no = commit_tests[idx]
289 260 for y in yes:
290 261 response.mustcontain(y)
291 262 for n in no:
292 263 response.mustcontain(no=n)
293 264
294 265 yes, no = diff_tests[idx]
295 266 for y in yes:
296 267 response.mustcontain(cb_line(y))
297 268 for n in no:
298 269 response.mustcontain(no=n)
299 270
300 271 # show diff between versions
301 272 diff_compare_tests = {
302 273 1: (['LINE3'], ['LINE1', 'LINE2']),
303 274 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
304 275 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
305 276 }
306 277 for idx, ver in enumerate(versions, 1):
307 278 adds, context = diff_compare_tests[idx]
308 279
309 280 to_ver = ver+1
310 281 if idx == 3:
311 282 to_ver = 'latest'
312 283
313 284 response = self.app.get(
314 285 route_path('pullrequest_show',
315 286 repo_name=target_repo_name,
316 287 pull_request_id=pull_request_id,
317 288 params={'from_version': versions[0], 'version': to_ver}))
318 289
319 290 response.mustcontain(no=['rhodecode diff rendering error'])
320 291
321 292 for a in adds:
322 293 response.mustcontain(cb_line(a))
323 294 for c in context:
324 295 response.mustcontain(cb_context(c))
325 296
326 297 # test version v2 -> v3
327 298 response = self.app.get(
328 299 route_path('pullrequest_show',
329 300 repo_name=target_repo_name,
330 301 pull_request_id=pull_request_id,
331 302 params={'from_version': versions[1], 'version': versions[2]}))
332 303
333 304 response.mustcontain(cb_context('LINE1'))
334 305 response.mustcontain(cb_context('LINE2'))
335 306 response.mustcontain(cb_context('LINE3'))
336 307 response.mustcontain(cb_line('LINE4'))
337 308
338 309 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
339 310 # Logout
340 311 response = self.app.post(
341 312 h.route_path('logout'),
342 313 params={'csrf_token': csrf_token})
343 314 # Login as regular user
344 315 response = self.app.post(h.route_path('login'),
345 316 {'username': TEST_USER_REGULAR_LOGIN,
346 317 'password': 'test12'})
347 318
348 319 pull_request = pr_util.create_pull_request(
349 320 author=TEST_USER_REGULAR_LOGIN)
350 321
351 322 response = self.app.get(route_path(
352 323 'pullrequest_show',
353 324 repo_name=pull_request.target_repo.scm_instance().name,
354 325 pull_request_id=pull_request.pull_request_id))
355 326
356 327 response.mustcontain('Server-side pull request merging is disabled.')
357 328
358 329 assert_response = response.assert_response()
359 330 # for regular user without a merge permissions, we don't see it
360 331 assert_response.no_element_exists('#close-pull-request-action')
361 332
362 333 user_util.grant_user_permission_to_repo(
363 334 pull_request.target_repo,
364 335 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
365 336 'repository.write')
366 337 response = self.app.get(route_path(
367 338 'pullrequest_show',
368 339 repo_name=pull_request.target_repo.scm_instance().name,
369 340 pull_request_id=pull_request.pull_request_id))
370 341
371 342 response.mustcontain('Server-side pull request merging is disabled.')
372 343
373 344 assert_response = response.assert_response()
374 345 # now regular user has a merge permissions, we have CLOSE button
375 346 assert_response.one_element_exists('#close-pull-request-action')
376 347
377 348 def test_show_invalid_commit_id(self, pr_util):
378 349 # Simulating invalid revisions which will cause a lookup error
379 350 pull_request = pr_util.create_pull_request()
380 351 pull_request.revisions = ['invalid']
381 352 Session().add(pull_request)
382 353 Session().commit()
383 354
384 355 response = self.app.get(route_path(
385 356 'pullrequest_show',
386 357 repo_name=pull_request.target_repo.scm_instance().name,
387 358 pull_request_id=pull_request.pull_request_id))
388 359
389 360 for commit_id in pull_request.revisions:
390 361 response.mustcontain(commit_id)
391 362
392 363 def test_show_invalid_source_reference(self, pr_util):
393 364 pull_request = pr_util.create_pull_request()
394 365 pull_request.source_ref = 'branch:b:invalid'
395 366 Session().add(pull_request)
396 367 Session().commit()
397 368
398 369 self.app.get(route_path(
399 370 'pullrequest_show',
400 371 repo_name=pull_request.target_repo.scm_instance().name,
401 372 pull_request_id=pull_request.pull_request_id))
402 373
403 374 def test_edit_title_description(self, pr_util, csrf_token):
404 375 pull_request = pr_util.create_pull_request()
405 376 pull_request_id = pull_request.pull_request_id
406 377
407 378 response = self.app.post(
408 379 route_path('pullrequest_update',
409 380 repo_name=pull_request.target_repo.repo_name,
410 381 pull_request_id=pull_request_id),
411 382 params={
412 383 'edit_pull_request': 'true',
413 384 'title': 'New title',
414 385 'description': 'New description',
415 386 'csrf_token': csrf_token})
416 387
417 388 assert_session_flash(
418 389 response, 'Pull request title & description updated.',
419 390 category='success')
420 391
421 392 pull_request = PullRequest.get(pull_request_id)
422 393 assert pull_request.title == 'New title'
423 394 assert pull_request.description == 'New description'
424 395
425 396 def test_edit_title_description_special(self, pr_util, csrf_token):
426 397 pull_request = pr_util.create_pull_request()
427 398 pull_request_id = pull_request.pull_request_id
428 399
429 400 response = self.app.post(
430 401 route_path('pullrequest_update',
431 402 repo_name=pull_request.target_repo.repo_name,
432 403 pull_request_id=pull_request_id),
433 404 params={
434 405 'edit_pull_request': 'true',
435 406 'title': 'New title {} {2} {foo}',
436 407 'description': 'New description',
437 408 'csrf_token': csrf_token})
438 409
439 410 assert_session_flash(
440 411 response, 'Pull request title & description updated.',
441 412 category='success')
442 413
443 414 pull_request = PullRequest.get(pull_request_id)
444 415 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
445 416
446 417 def test_edit_title_description_closed(self, pr_util, csrf_token):
447 418 pull_request = pr_util.create_pull_request()
448 419 pull_request_id = pull_request.pull_request_id
449 420 repo_name = pull_request.target_repo.repo_name
450 421 pr_util.close()
451 422
452 423 response = self.app.post(
453 424 route_path('pullrequest_update',
454 425 repo_name=repo_name, pull_request_id=pull_request_id),
455 426 params={
456 427 'edit_pull_request': 'true',
457 428 'title': 'New title',
458 429 'description': 'New description',
459 430 'csrf_token': csrf_token}, status=200)
460 431 assert_session_flash(
461 432 response, 'Cannot update closed pull requests.',
462 433 category='error')
463 434
464 435 def test_update_invalid_source_reference(self, pr_util, csrf_token):
465 436 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
466 437
467 438 pull_request = pr_util.create_pull_request()
468 439 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
469 440 Session().add(pull_request)
470 441 Session().commit()
471 442
472 443 pull_request_id = pull_request.pull_request_id
473 444
474 445 response = self.app.post(
475 446 route_path('pullrequest_update',
476 447 repo_name=pull_request.target_repo.repo_name,
477 448 pull_request_id=pull_request_id),
478 449 params={'update_commits': 'true', 'csrf_token': csrf_token})
479 450
480 451 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
481 452 UpdateFailureReason.MISSING_SOURCE_REF])
482 453 assert_session_flash(response, expected_msg, category='error')
483 454
484 455 def test_missing_target_reference(self, pr_util, csrf_token):
485 456 from rhodecode.lib.vcs.backends.base import MergeFailureReason
486 457 pull_request = pr_util.create_pull_request(
487 458 approved=True, mergeable=True)
488 459 unicode_reference = 'branch:invalid-branch:invalid-commit-id'
489 460 pull_request.target_ref = unicode_reference
490 461 Session().add(pull_request)
491 462 Session().commit()
492 463
493 464 pull_request_id = pull_request.pull_request_id
494 465 pull_request_url = route_path(
495 466 'pullrequest_show',
496 467 repo_name=pull_request.target_repo.repo_name,
497 468 pull_request_id=pull_request_id)
498 469
499 470 response = self.app.get(pull_request_url)
500 471 target_ref_id = 'invalid-branch'
501 472 merge_resp = MergeResponse(
502 473 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
503 474 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
504 475 response.assert_response().element_contains(
505 476 'div[data-role="merge-message"]', merge_resp.merge_status_message)
506 477
507 478 def test_comment_and_close_pull_request_custom_message_approved(
508 479 self, pr_util, csrf_token, xhr_header):
509 480
510 481 pull_request = pr_util.create_pull_request(approved=True)
511 482 pull_request_id = pull_request.pull_request_id
512 483 author = pull_request.user_id
513 484 repo = pull_request.target_repo.repo_id
514 485
515 486 self.app.post(
516 487 route_path('pullrequest_comment_create',
517 488 repo_name=pull_request.target_repo.scm_instance().name,
518 489 pull_request_id=pull_request_id),
519 490 params={
520 491 'close_pull_request': '1',
521 492 'text': 'Closing a PR',
522 493 'csrf_token': csrf_token},
523 494 extra_environ=xhr_header,)
524 495
525 496 journal = UserLog.query()\
526 497 .filter(UserLog.user_id == author)\
527 498 .filter(UserLog.repository_id == repo) \
528 499 .order_by(UserLog.user_log_id.asc()) \
529 500 .all()
530 501 assert journal[-1].action == 'repo.pull_request.close'
531 502
532 503 pull_request = PullRequest.get(pull_request_id)
533 504 assert pull_request.is_closed()
534 505
535 506 status = ChangesetStatusModel().get_status(
536 507 pull_request.source_repo, pull_request=pull_request)
537 508 assert status == ChangesetStatus.STATUS_APPROVED
538 509 comments = ChangesetComment().query() \
539 510 .filter(ChangesetComment.pull_request == pull_request) \
540 511 .order_by(ChangesetComment.comment_id.asc())\
541 512 .all()
542 513 assert comments[-1].text == 'Closing a PR'
543 514
544 515 def test_comment_force_close_pull_request_rejected(
545 516 self, pr_util, csrf_token, xhr_header):
546 517 pull_request = pr_util.create_pull_request()
547 518 pull_request_id = pull_request.pull_request_id
548 519 PullRequestModel().update_reviewers(
549 520 pull_request_id, [
550 521 (1, ['reason'], False, 'reviewer', []),
551 522 (2, ['reason2'], False, 'reviewer', [])],
552 523 pull_request.author)
553 524 author = pull_request.user_id
554 525 repo = pull_request.target_repo.repo_id
555 526
556 527 self.app.post(
557 528 route_path('pullrequest_comment_create',
558 529 repo_name=pull_request.target_repo.scm_instance().name,
559 530 pull_request_id=pull_request_id),
560 531 params={
561 532 'close_pull_request': '1',
562 533 'csrf_token': csrf_token},
563 534 extra_environ=xhr_header)
564 535
565 536 pull_request = PullRequest.get(pull_request_id)
566 537
567 538 journal = UserLog.query()\
568 539 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
569 540 .order_by(UserLog.user_log_id.asc()) \
570 541 .all()
571 542 assert journal[-1].action == 'repo.pull_request.close'
572 543
573 544 # check only the latest status, not the review status
574 545 status = ChangesetStatusModel().get_status(
575 546 pull_request.source_repo, pull_request=pull_request)
576 547 assert status == ChangesetStatus.STATUS_REJECTED
577 548
578 549 def test_comment_and_close_pull_request(
579 550 self, pr_util, csrf_token, xhr_header):
580 551 pull_request = pr_util.create_pull_request()
581 552 pull_request_id = pull_request.pull_request_id
582 553
583 554 response = self.app.post(
584 555 route_path('pullrequest_comment_create',
585 556 repo_name=pull_request.target_repo.scm_instance().name,
586 557 pull_request_id=pull_request.pull_request_id),
587 558 params={
588 559 'close_pull_request': 'true',
589 560 'csrf_token': csrf_token},
590 561 extra_environ=xhr_header)
591 562
592 563 assert response.json
593 564
594 565 pull_request = PullRequest.get(pull_request_id)
595 566 assert pull_request.is_closed()
596 567
597 568 # check only the latest status, not the review status
598 569 status = ChangesetStatusModel().get_status(
599 570 pull_request.source_repo, pull_request=pull_request)
600 571 assert status == ChangesetStatus.STATUS_REJECTED
601 572
602 573 def test_comment_and_close_pull_request_try_edit_comment(
603 574 self, pr_util, csrf_token, xhr_header
604 575 ):
605 576 pull_request = pr_util.create_pull_request()
606 577 pull_request_id = pull_request.pull_request_id
607 578 target_scm = pull_request.target_repo.scm_instance()
608 579 target_scm_name = target_scm.name
609 580
610 581 response = self.app.post(
611 582 route_path(
612 583 'pullrequest_comment_create',
613 584 repo_name=target_scm_name,
614 585 pull_request_id=pull_request_id,
615 586 ),
616 587 params={
617 588 'close_pull_request': 'true',
618 589 'csrf_token': csrf_token,
619 590 },
620 591 extra_environ=xhr_header)
621 592
622 593 assert response.json
623 594
624 595 pull_request = PullRequest.get(pull_request_id)
625 596 target_scm = pull_request.target_repo.scm_instance()
626 597 target_scm_name = target_scm.name
627 598 assert pull_request.is_closed()
628 599
629 600 # check only the latest status, not the review status
630 601 status = ChangesetStatusModel().get_status(
631 602 pull_request.source_repo, pull_request=pull_request)
632 603 assert status == ChangesetStatus.STATUS_REJECTED
633 604
634 605 for comment_id in response.json.keys():
635 606 test_text = 'test'
636 607 response = self.app.post(
637 608 route_path(
638 609 'pullrequest_comment_edit',
639 610 repo_name=target_scm_name,
640 611 pull_request_id=pull_request_id,
641 612 comment_id=comment_id,
642 613 ),
643 614 extra_environ=xhr_header,
644 615 params={
645 616 'csrf_token': csrf_token,
646 617 'text': test_text,
647 618 },
648 619 status=403,
649 620 )
650 621 assert response.status_int == 403
651 622
652 623 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
653 624 pull_request = pr_util.create_pull_request()
654 625 target_scm = pull_request.target_repo.scm_instance()
655 626 target_scm_name = target_scm.name
656 627
657 628 response = self.app.post(
658 629 route_path(
659 630 'pullrequest_comment_create',
660 631 repo_name=target_scm_name,
661 632 pull_request_id=pull_request.pull_request_id),
662 633 params={
663 634 'csrf_token': csrf_token,
664 635 'text': 'init',
665 636 },
666 637 extra_environ=xhr_header,
667 638 )
668 639 assert response.json
669 640
670 641 for comment_id in response.json.keys():
671 642 assert comment_id
672 643 test_text = 'test'
673 644 self.app.post(
674 645 route_path(
675 646 'pullrequest_comment_edit',
676 647 repo_name=target_scm_name,
677 648 pull_request_id=pull_request.pull_request_id,
678 649 comment_id=comment_id,
679 650 ),
680 651 extra_environ=xhr_header,
681 652 params={
682 653 'csrf_token': csrf_token,
683 654 'text': test_text,
684 655 'version': '0',
685 656 },
686 657
687 658 )
688 659 text_form_db = ChangesetComment.query().filter(
689 660 ChangesetComment.comment_id == comment_id).first().text
690 661 assert test_text == text_form_db
691 662
692 663 def test_comment_and_comment_edit_special(self, pr_util, csrf_token, xhr_header):
693 664 pull_request = pr_util.create_pull_request()
694 665 target_scm = pull_request.target_repo.scm_instance()
695 666 target_scm_name = target_scm.name
696 667
697 668 response = self.app.post(
698 669 route_path(
699 670 'pullrequest_comment_create',
700 671 repo_name=target_scm_name,
701 672 pull_request_id=pull_request.pull_request_id),
702 673 params={
703 674 'csrf_token': csrf_token,
704 675 'text': 'init',
705 676 },
706 677 extra_environ=xhr_header,
707 678 )
708 679 assert response.json
709 680
710 681 for comment_id in response.json.keys():
711 682 test_text = 'init'
712 683 response = self.app.post(
713 684 route_path(
714 685 'pullrequest_comment_edit',
715 686 repo_name=target_scm_name,
716 687 pull_request_id=pull_request.pull_request_id,
717 688 comment_id=comment_id,
718 689 ),
719 690 extra_environ=xhr_header,
720 691 params={
721 692 'csrf_token': csrf_token,
722 693 'text': test_text,
723 694 'version': '0',
724 695 },
725 696 status=404,
726 697
727 698 )
728 699 assert response.status_int == 404
729 700
730 701 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
731 702 pull_request = pr_util.create_pull_request()
732 703 target_scm = pull_request.target_repo.scm_instance()
733 704 target_scm_name = target_scm.name
734 705
735 706 response = self.app.post(
736 707 route_path(
737 708 'pullrequest_comment_create',
738 709 repo_name=target_scm_name,
739 710 pull_request_id=pull_request.pull_request_id),
740 711 params={
741 712 'csrf_token': csrf_token,
742 713 'text': 'init',
743 714 },
744 715 extra_environ=xhr_header,
745 716 )
746 717 assert response.json
747 718 for comment_id in response.json.keys():
748 719 test_text = 'test'
749 720 self.app.post(
750 721 route_path(
751 722 'pullrequest_comment_edit',
752 723 repo_name=target_scm_name,
753 724 pull_request_id=pull_request.pull_request_id,
754 725 comment_id=comment_id,
755 726 ),
756 727 extra_environ=xhr_header,
757 728 params={
758 729 'csrf_token': csrf_token,
759 730 'text': test_text,
760 731 'version': '0',
761 732 },
762 733
763 734 )
764 735 test_text_v2 = 'test_v2'
765 736 response = self.app.post(
766 737 route_path(
767 738 'pullrequest_comment_edit',
768 739 repo_name=target_scm_name,
769 740 pull_request_id=pull_request.pull_request_id,
770 741 comment_id=comment_id,
771 742 ),
772 743 extra_environ=xhr_header,
773 744 params={
774 745 'csrf_token': csrf_token,
775 746 'text': test_text_v2,
776 747 'version': '0',
777 748 },
778 749 status=409,
779 750 )
780 751 assert response.status_int == 409
781 752
782 753 text_form_db = ChangesetComment.query().filter(
783 754 ChangesetComment.comment_id == comment_id).first().text
784 755
785 756 assert test_text == text_form_db
786 757 assert test_text_v2 != text_form_db
787 758
788 759 def test_comment_and_comment_edit_permissions_forbidden(
789 760 self, autologin_regular_user, user_regular, user_admin, pr_util,
790 761 csrf_token, xhr_header):
791 762 pull_request = pr_util.create_pull_request(
792 763 author=user_admin.username, enable_notifications=False)
793 764 comment = CommentsModel().create(
794 765 text='test',
795 766 repo=pull_request.target_repo.scm_instance().name,
796 767 user=user_admin,
797 768 pull_request=pull_request,
798 769 )
799 770 response = self.app.post(
800 771 route_path(
801 772 'pullrequest_comment_edit',
802 773 repo_name=pull_request.target_repo.scm_instance().name,
803 774 pull_request_id=pull_request.pull_request_id,
804 775 comment_id=comment.comment_id,
805 776 ),
806 777 extra_environ=xhr_header,
807 778 params={
808 779 'csrf_token': csrf_token,
809 780 'text': 'test_text',
810 781 },
811 782 status=403,
812 783 )
813 784 assert response.status_int == 403
814 785
815 786 def test_create_pull_request(self, backend, csrf_token):
816 787 commits = [
817 788 {'message': 'ancestor'},
818 789 {'message': 'change'},
819 790 {'message': 'change2'},
820 791 ]
821 792 commit_ids = backend.create_master_repo(commits)
822 793 target = backend.create_repo(heads=['ancestor'])
823 794 source = backend.create_repo(heads=['change2'])
824 795
825 796 response = self.app.post(
826 797 route_path('pullrequest_create', repo_name=source.repo_name),
827 798 [
828 799 ('source_repo', source.repo_name),
829 800 ('source_ref', 'branch:default:' + commit_ids['change2']),
830 801 ('target_repo', target.repo_name),
831 802 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
832 803 ('common_ancestor', commit_ids['ancestor']),
833 804 ('pullrequest_title', 'Title'),
834 805 ('pullrequest_desc', 'Description'),
835 806 ('description_renderer', 'markdown'),
836 807 ('__start__', 'review_members:sequence'),
837 808 ('__start__', 'reviewer:mapping'),
838 809 ('user_id', '1'),
839 810 ('__start__', 'reasons:sequence'),
840 811 ('reason', 'Some reason'),
841 812 ('__end__', 'reasons:sequence'),
842 813 ('__start__', 'rules:sequence'),
843 814 ('__end__', 'rules:sequence'),
844 815 ('mandatory', 'False'),
845 816 ('__end__', 'reviewer:mapping'),
846 817 ('__end__', 'review_members:sequence'),
847 818 ('__start__', 'revisions:sequence'),
848 819 ('revisions', commit_ids['change']),
849 820 ('revisions', commit_ids['change2']),
850 821 ('__end__', 'revisions:sequence'),
851 822 ('user', ''),
852 823 ('csrf_token', csrf_token),
853 824 ],
854 825 status=302)
855 826
856 827 location = response.headers['Location']
857 828 pull_request_id = location.rsplit('/', 1)[1]
858 829 assert pull_request_id != 'new'
859 830 pull_request = PullRequest.get(int(pull_request_id))
860 831
861 832 # check that we have now both revisions
862 833 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
863 834 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
864 835 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
865 836 assert pull_request.target_ref == expected_target_ref
866 837
867 838 def test_reviewer_notifications(self, backend, csrf_token):
868 839 # We have to use the app.post for this test so it will create the
869 840 # notifications properly with the new PR
870 841 commits = [
871 842 {'message': 'ancestor',
872 843 'added': [FileNode(b'file_A', content=b'content_of_ancestor')]},
873 844 {'message': 'change',
874 845 'added': [FileNode(b'file_a', content=b'content_of_change')]},
875 846 {'message': 'change-child'},
876 847 {'message': 'ancestor-child', 'parents': ['ancestor'],
877 848 'added': [ FileNode(b'file_B', content=b'content_of_ancestor_child')]},
878 849 {'message': 'ancestor-child-2'},
879 850 ]
880 851 commit_ids = backend.create_master_repo(commits)
881 852 target = backend.create_repo(heads=['ancestor-child'])
882 853 source = backend.create_repo(heads=['change'])
883 854
884 855 response = self.app.post(
885 856 route_path('pullrequest_create', repo_name=source.repo_name),
886 857 [
887 858 ('source_repo', source.repo_name),
888 859 ('source_ref', 'branch:default:' + commit_ids['change']),
889 860 ('target_repo', target.repo_name),
890 861 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
891 862 ('common_ancestor', commit_ids['ancestor']),
892 863 ('pullrequest_title', 'Title'),
893 864 ('pullrequest_desc', 'Description'),
894 865 ('description_renderer', 'markdown'),
895 866 ('__start__', 'review_members:sequence'),
896 867 ('__start__', 'reviewer:mapping'),
897 868 ('user_id', '2'),
898 869 ('__start__', 'reasons:sequence'),
899 870 ('reason', 'Some reason'),
900 871 ('__end__', 'reasons:sequence'),
901 872 ('__start__', 'rules:sequence'),
902 873 ('__end__', 'rules:sequence'),
903 874 ('mandatory', 'False'),
904 875 ('__end__', 'reviewer:mapping'),
905 876 ('__end__', 'review_members:sequence'),
906 877 ('__start__', 'revisions:sequence'),
907 878 ('revisions', commit_ids['change']),
908 879 ('__end__', 'revisions:sequence'),
909 880 ('user', ''),
910 881 ('csrf_token', csrf_token),
911 882 ],
912 883 status=302)
913 884
914 885 location = response.headers['Location']
915 886
916 887 pull_request_id = location.rsplit('/', 1)[1]
917 888 assert pull_request_id != 'new'
918 889 pull_request = PullRequest.get(int(pull_request_id))
919 890
920 891 # Check that a notification was made
921 892 notifications = Notification.query()\
922 893 .filter(Notification.created_by == pull_request.author.user_id,
923 894 Notification.type_ == Notification.TYPE_PULL_REQUEST,
924 895 Notification.subject.contains(
925 896 "requested a pull request review. !%s" % pull_request_id))
926 897 assert len(notifications.all()) == 1
927 898
928 899 # Change reviewers and check that a notification was made
929 900 PullRequestModel().update_reviewers(
930 901 pull_request.pull_request_id, [
931 902 (1, [], False, 'reviewer', [])
932 903 ],
933 904 pull_request.author)
934 905 assert len(notifications.all()) == 2
935 906
936 907 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
937 908 commits = [
938 909 {'message': 'ancestor',
939 910 'added': [FileNode(b'file_A', content=b'content_of_ancestor')]},
940 911 {'message': 'change',
941 912 'added': [FileNode(b'file_a', content=b'content_of_change')]},
942 913 {'message': 'change-child'},
943 914 {'message': 'ancestor-child', 'parents': ['ancestor'],
944 915 'added': [
945 916 FileNode(b'file_B', content=b'content_of_ancestor_child')]},
946 917 {'message': 'ancestor-child-2'},
947 918 ]
948 919 commit_ids = backend.create_master_repo(commits)
949 920 target = backend.create_repo(heads=['ancestor-child'])
950 921 source = backend.create_repo(heads=['change'])
951 922
952 923 response = self.app.post(
953 924 route_path('pullrequest_create', repo_name=source.repo_name),
954 925 [
955 926 ('source_repo', source.repo_name),
956 927 ('source_ref', 'branch:default:' + commit_ids['change']),
957 928 ('target_repo', target.repo_name),
958 929 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
959 930 ('common_ancestor', commit_ids['ancestor']),
960 931 ('pullrequest_title', 'Title'),
961 932 ('pullrequest_desc', 'Description'),
962 933 ('description_renderer', 'markdown'),
963 934 ('__start__', 'review_members:sequence'),
964 935 ('__start__', 'reviewer:mapping'),
965 936 ('user_id', '1'),
966 937 ('__start__', 'reasons:sequence'),
967 938 ('reason', 'Some reason'),
968 939 ('__end__', 'reasons:sequence'),
969 940 ('__start__', 'rules:sequence'),
970 941 ('__end__', 'rules:sequence'),
971 942 ('mandatory', 'False'),
972 943 ('__end__', 'reviewer:mapping'),
973 944 ('__end__', 'review_members:sequence'),
974 945 ('__start__', 'revisions:sequence'),
975 946 ('revisions', commit_ids['change']),
976 947 ('__end__', 'revisions:sequence'),
977 948 ('user', ''),
978 949 ('csrf_token', csrf_token),
979 950 ],
980 951 status=302)
981 952
982 953 location = response.headers['Location']
983 954
984 955 pull_request_id = location.rsplit('/', 1)[1]
985 956 assert pull_request_id != 'new'
986 957 pull_request = PullRequest.get(int(pull_request_id))
987 958
988 959 # target_ref has to point to the ancestor's commit_id in order to
989 960 # show the correct diff
990 961 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
991 962 assert pull_request.target_ref == expected_target_ref
992 963
993 964 # Check generated diff contents
994 965 response = response.follow()
995 966 response.mustcontain(no=['content_of_ancestor'])
996 967 response.mustcontain(no=['content_of_ancestor-child'])
997 968 response.mustcontain('content_of_change')
998 969
999 970 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
1000 971 # Clear any previous calls to rcextensions
1001 972 rhodecode.EXTENSIONS.calls.clear()
1002 973
1003 974 pull_request = pr_util.create_pull_request(
1004 975 approved=True, mergeable=True)
1005 976 pull_request_id = pull_request.pull_request_id
1006 977 repo_name = pull_request.target_repo.scm_instance().name,
1007 978
1008 979 url = route_path('pullrequest_merge',
1009 980 repo_name=str(repo_name[0]),
1010 981 pull_request_id=pull_request_id)
1011 982 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1012 983
1013 984 pull_request = PullRequest.get(pull_request_id)
1014 985
1015 986 assert response.status_int == 200
1016 987 assert pull_request.is_closed()
1017 988 assert_pull_request_status(
1018 989 pull_request, ChangesetStatus.STATUS_APPROVED)
1019 990
1020 991 # Check the relevant log entries were added
1021 992 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1022 993 actions = [log.action for log in user_logs]
1023 994 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1024 995 expected_actions = [
1025 996 'repo.pull_request.close',
1026 997 'repo.pull_request.merge',
1027 998 'repo.pull_request.comment.create'
1028 999 ]
1029 1000 assert actions == expected_actions
1030 1001
1031 1002 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1032 1003 actions = [log for log in user_logs]
1033 1004 assert actions[-1].action == 'user.push'
1034 1005 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1035 1006
1036 1007 # Check post_push rcextension was really executed
1037 1008 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1038 1009 assert len(push_calls) == 1
1039 1010 unused_last_call_args, last_call_kwargs = push_calls[0]
1040 1011 assert last_call_kwargs['action'] == 'push'
1041 1012 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1042 1013
1043 1014 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1044 1015 pull_request = pr_util.create_pull_request(mergeable=False)
1045 1016 pull_request_id = pull_request.pull_request_id
1046 1017 pull_request = PullRequest.get(pull_request_id)
1047 1018
1048 1019 response = self.app.post(
1049 1020 route_path('pullrequest_merge',
1050 1021 repo_name=pull_request.target_repo.scm_instance().name,
1051 1022 pull_request_id=pull_request.pull_request_id),
1052 1023 params={'csrf_token': csrf_token}).follow()
1053 1024
1054 1025 assert response.status_int == 200
1055 1026 response.mustcontain(
1056 1027 'Merge is not currently possible because of below failed checks.')
1057 1028 response.mustcontain('Server-side pull request merging is disabled.')
1058 1029
1059 1030 @pytest.mark.skip_backends('svn')
1060 1031 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1061 1032 pull_request = pr_util.create_pull_request(mergeable=True)
1062 1033 pull_request_id = pull_request.pull_request_id
1063 1034 repo_name = pull_request.target_repo.scm_instance().name
1064 1035
1065 1036 response = self.app.post(
1066 1037 route_path('pullrequest_merge',
1067 1038 repo_name=repo_name, pull_request_id=pull_request_id),
1068 1039 params={'csrf_token': csrf_token}).follow()
1069 1040
1070 1041 assert response.status_int == 200
1071 1042
1072 1043 response.mustcontain(
1073 1044 'Merge is not currently possible because of below failed checks.')
1074 1045 response.mustcontain('Pull request reviewer approval is pending.')
1075 1046
1076 1047 def test_merge_pull_request_renders_failure_reason(
1077 1048 self, user_regular, csrf_token, pr_util):
1078 1049 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1079 1050 pull_request_id = pull_request.pull_request_id
1080 1051 repo_name = pull_request.target_repo.scm_instance().name
1081 1052
1082 1053 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1083 1054 MergeFailureReason.PUSH_FAILED,
1084 1055 metadata={'target': 'shadow repo',
1085 1056 'merge_commit': 'xxx'})
1086 1057 model_patcher = mock.patch.multiple(
1087 1058 PullRequestModel,
1088 1059 merge_repo=mock.Mock(return_value=merge_resp),
1089 1060 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1090 1061
1091 1062 with model_patcher:
1092 1063 response = self.app.post(
1093 1064 route_path('pullrequest_merge',
1094 1065 repo_name=repo_name,
1095 1066 pull_request_id=pull_request_id),
1096 1067 params={'csrf_token': csrf_token}, status=302)
1097 1068
1098 1069 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1099 1070 metadata={'target': 'shadow repo',
1100 1071 'merge_commit': 'xxx'})
1101 1072 assert_session_flash(response, merge_resp.merge_status_message)
1102 1073
1103 1074 def test_update_source_revision(self, backend, csrf_token):
1104 1075 commits = [
1105 1076 {'message': 'ancestor'},
1106 1077 {'message': 'change'},
1107 1078 {'message': 'change-2'},
1108 1079 ]
1109 1080 commit_ids = backend.create_master_repo(commits)
1110 1081 target = backend.create_repo(heads=['ancestor'])
1111 1082 source = backend.create_repo(heads=['change'])
1112 1083
1113 1084 # create pr from a in source to A in target
1114 1085 pull_request = PullRequest()
1115 1086
1116 1087 pull_request.source_repo = source
1117 1088 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1118 1089 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1119 1090
1120 1091 pull_request.target_repo = target
1121 1092 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1122 1093 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1123 1094
1124 1095 pull_request.revisions = [commit_ids['change']]
1125 1096 pull_request.title = "Test"
1126 1097 pull_request.description = "Description"
1127 1098 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1128 1099 pull_request.pull_request_state = PullRequest.STATE_CREATED
1129 1100 Session().add(pull_request)
1130 1101 Session().commit()
1131 1102 pull_request_id = pull_request.pull_request_id
1132 1103
1133 1104 # source has ancestor - change - change-2
1134 1105 backend.pull_heads(source, heads=['change-2'])
1135 1106 target_repo_name = target.repo_name
1136 1107
1137 1108 # update PR
1138 1109 self.app.post(
1139 1110 route_path('pullrequest_update',
1140 1111 repo_name=target_repo_name, pull_request_id=pull_request_id),
1141 1112 params={'update_commits': 'true', 'csrf_token': csrf_token})
1142 1113
1143 1114 response = self.app.get(
1144 1115 route_path('pullrequest_show',
1145 1116 repo_name=target_repo_name,
1146 1117 pull_request_id=pull_request.pull_request_id))
1147 1118
1148 1119 assert response.status_int == 200
1149 1120 response.mustcontain('Pull request updated to')
1150 1121 response.mustcontain('with 1 added, 0 removed commits.')
1151 1122
1152 1123 # check that we have now both revisions
1153 1124 pull_request = PullRequest.get(pull_request_id)
1154 1125 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1155 1126
1156 1127 def test_update_target_revision(self, backend, csrf_token):
1157 1128 commits = [
1158 1129 {'message': 'ancestor'},
1159 1130 {'message': 'change'},
1160 1131 {'message': 'ancestor-new', 'parents': ['ancestor']},
1161 1132 {'message': 'change-rebased'},
1162 1133 ]
1163 1134 commit_ids = backend.create_master_repo(commits)
1164 1135 target = backend.create_repo(heads=['ancestor'])
1165 1136 source = backend.create_repo(heads=['change'])
1166 1137
1167 1138 # create pr from a in source to A in target
1168 1139 pull_request = PullRequest()
1169 1140
1170 1141 pull_request.source_repo = source
1171 1142 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1172 1143 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1173 1144
1174 1145 pull_request.target_repo = target
1175 1146 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1176 1147 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1177 1148
1178 1149 pull_request.revisions = [commit_ids['change']]
1179 1150 pull_request.title = "Test"
1180 1151 pull_request.description = "Description"
1181 1152 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1182 1153 pull_request.pull_request_state = PullRequest.STATE_CREATED
1183 1154
1184 1155 Session().add(pull_request)
1185 1156 Session().commit()
1186 1157 pull_request_id = pull_request.pull_request_id
1187 1158
1188 1159 # target has ancestor - ancestor-new
1189 1160 # source has ancestor - ancestor-new - change-rebased
1190 1161 backend.pull_heads(target, heads=['ancestor-new'])
1191 1162 backend.pull_heads(source, heads=['change-rebased'])
1192 1163 target_repo_name = target.repo_name
1193 1164
1194 1165 # update PR
1195 1166 url = route_path('pullrequest_update',
1196 1167 repo_name=target_repo_name,
1197 1168 pull_request_id=pull_request_id)
1198 1169 self.app.post(url,
1199 1170 params={'update_commits': 'true', 'csrf_token': csrf_token},
1200 1171 status=200)
1201 1172
1202 1173 # check that we have now both revisions
1203 1174 pull_request = PullRequest.get(pull_request_id)
1204 1175 assert pull_request.revisions == [commit_ids['change-rebased']]
1205 1176 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1206 1177 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1207 1178
1208 1179 response = self.app.get(
1209 1180 route_path('pullrequest_show',
1210 1181 repo_name=target_repo_name,
1211 1182 pull_request_id=pull_request.pull_request_id))
1212 1183 assert response.status_int == 200
1213 1184 response.mustcontain('Pull request updated to')
1214 1185 response.mustcontain('with 1 added, 1 removed commits.')
1215 1186
1216 1187 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1217 1188 backend = backend_git
1218 1189 commits = [
1219 1190 {'message': 'master-commit-1'},
1220 1191 {'message': 'master-commit-2-change-1'},
1221 1192 {'message': 'master-commit-3-change-2'},
1222 1193
1223 1194 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1224 1195 {'message': 'feat-commit-2'},
1225 1196 ]
1226 1197 commit_ids = backend.create_master_repo(commits)
1227 1198 target = backend.create_repo(heads=['master-commit-3-change-2'])
1228 1199 source = backend.create_repo(heads=['feat-commit-2'])
1229 1200
1230 1201 # create pr from a in source to A in target
1231 1202 pull_request = PullRequest()
1232 1203 pull_request.source_repo = source
1233 1204
1234 1205 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1235 1206 branch=backend.default_branch_name,
1236 1207 commit_id=commit_ids['master-commit-3-change-2'])
1237 1208
1238 1209 pull_request.target_repo = target
1239 1210 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1240 1211 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1241 1212
1242 1213 pull_request.revisions = [
1243 1214 commit_ids['feat-commit-1'],
1244 1215 commit_ids['feat-commit-2']
1245 1216 ]
1246 1217 pull_request.title = "Test"
1247 1218 pull_request.description = "Description"
1248 1219 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1249 1220 pull_request.pull_request_state = PullRequest.STATE_CREATED
1250 1221 Session().add(pull_request)
1251 1222 Session().commit()
1252 1223 pull_request_id = pull_request.pull_request_id
1253 1224
1254 1225 # PR is created, now we simulate a force-push into target,
1255 1226 # that drops a 2 last commits
1256 1227 vcsrepo = target.scm_instance()
1257 1228 vcsrepo.config.clear_section('hooks')
1258 1229 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1259 1230 target_repo_name = target.repo_name
1260 1231
1261 1232 # update PR
1262 1233 url = route_path('pullrequest_update',
1263 1234 repo_name=target_repo_name,
1264 1235 pull_request_id=pull_request_id)
1265 1236 self.app.post(url,
1266 1237 params={'update_commits': 'true', 'csrf_token': csrf_token},
1267 1238 status=200)
1268 1239
1269 1240 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1270 1241 assert response.status_int == 200
1271 1242 response.mustcontain('Pull request updated to')
1272 1243 response.mustcontain('with 0 added, 0 removed commits.')
1273 1244
1274 1245 def test_update_of_ancestor_reference(self, backend, csrf_token):
1275 1246 commits = [
1276 1247 {'message': 'ancestor'},
1277 1248 {'message': 'change'},
1278 1249 {'message': 'change-2'},
1279 1250 {'message': 'ancestor-new', 'parents': ['ancestor']},
1280 1251 {'message': 'change-rebased'},
1281 1252 ]
1282 1253 commit_ids = backend.create_master_repo(commits)
1283 1254 target = backend.create_repo(heads=['ancestor'])
1284 1255 source = backend.create_repo(heads=['change'])
1285 1256
1286 1257 # create pr from a in source to A in target
1287 1258 pull_request = PullRequest()
1288 1259 pull_request.source_repo = source
1289 1260
1290 1261 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1291 1262 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1292 1263 pull_request.target_repo = target
1293 1264 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1294 1265 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1295 1266 pull_request.revisions = [commit_ids['change']]
1296 1267 pull_request.title = "Test"
1297 1268 pull_request.description = "Description"
1298 1269 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1299 1270 pull_request.pull_request_state = PullRequest.STATE_CREATED
1300 1271 Session().add(pull_request)
1301 1272 Session().commit()
1302 1273 pull_request_id = pull_request.pull_request_id
1303 1274
1304 1275 # target has ancestor - ancestor-new
1305 1276 # source has ancestor - ancestor-new - change-rebased
1306 1277 backend.pull_heads(target, heads=['ancestor-new'])
1307 1278 backend.pull_heads(source, heads=['change-rebased'])
1308 1279 target_repo_name = target.repo_name
1309 1280
1310 1281 # update PR
1311 1282 self.app.post(
1312 1283 route_path('pullrequest_update',
1313 1284 repo_name=target_repo_name, pull_request_id=pull_request_id),
1314 1285 params={'update_commits': 'true', 'csrf_token': csrf_token},
1315 1286 status=200)
1316 1287
1317 1288 # Expect the target reference to be updated correctly
1318 1289 pull_request = PullRequest.get(pull_request_id)
1319 1290 assert pull_request.revisions == [commit_ids['change-rebased']]
1320 1291 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1321 1292 branch=backend.default_branch_name,
1322 1293 commit_id=commit_ids['ancestor-new'])
1323 1294 assert pull_request.target_ref == expected_target_ref
1324 1295
1325 1296 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1326 1297 branch_name = 'development'
1327 1298 commits = [
1328 1299 {'message': 'initial-commit'},
1329 1300 {'message': 'old-feature'},
1330 1301 {'message': 'new-feature', 'branch': branch_name},
1331 1302 ]
1332 1303 repo = backend_git.create_repo(commits)
1333 1304 repo_name = repo.repo_name
1334 1305 commit_ids = backend_git.commit_ids
1335 1306
1336 1307 pull_request = PullRequest()
1337 1308 pull_request.source_repo = repo
1338 1309 pull_request.target_repo = repo
1339 1310 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1340 1311 branch=branch_name, commit_id=commit_ids['new-feature'])
1341 1312 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1342 1313 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1343 1314 pull_request.revisions = [commit_ids['new-feature']]
1344 1315 pull_request.title = "Test"
1345 1316 pull_request.description = "Description"
1346 1317 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1347 1318 pull_request.pull_request_state = PullRequest.STATE_CREATED
1348 1319 Session().add(pull_request)
1349 1320 Session().commit()
1350 1321
1351 1322 pull_request_id = pull_request.pull_request_id
1352 1323
1353 1324 vcs = repo.scm_instance()
1354 1325 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1355 1326 # NOTE(marcink): run GC to ensure the commits are gone
1356 1327 vcs.run_gc()
1357 1328
1358 1329 response = self.app.get(route_path(
1359 1330 'pullrequest_show',
1360 1331 repo_name=repo_name,
1361 1332 pull_request_id=pull_request_id))
1362 1333
1363 1334 assert response.status_int == 200
1364 1335
1365 1336 response.assert_response().element_contains(
1366 1337 '#changeset_compare_view_content .alert strong',
1367 1338 'Missing commits')
1368 1339 response.assert_response().element_contains(
1369 1340 '#changeset_compare_view_content .alert',
1370 1341 'This pull request cannot be displayed, because one or more'
1371 1342 ' commits no longer exist in the source repository.')
1372 1343
1373 1344 def test_strip_commits_from_pull_request(
1374 1345 self, backend, pr_util, csrf_token):
1375 1346 commits = [
1376 1347 {'message': 'initial-commit'},
1377 1348 {'message': 'old-feature'},
1378 1349 {'message': 'new-feature', 'parents': ['initial-commit']},
1379 1350 ]
1380 1351 pull_request = pr_util.create_pull_request(
1381 1352 commits, target_head='initial-commit', source_head='new-feature',
1382 1353 revisions=['new-feature'])
1383 1354
1384 1355 vcs = pr_util.source_repository.scm_instance()
1385 1356 if backend.alias == 'git':
1386 1357 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1387 1358 else:
1388 1359 vcs.strip(pr_util.commit_ids['new-feature'])
1389 1360
1390 1361 response = self.app.get(route_path(
1391 1362 'pullrequest_show',
1392 1363 repo_name=pr_util.target_repository.repo_name,
1393 1364 pull_request_id=pull_request.pull_request_id))
1394 1365
1395 1366 assert response.status_int == 200
1396 1367
1397 1368 response.assert_response().element_contains(
1398 1369 '#changeset_compare_view_content .alert strong',
1399 1370 'Missing commits')
1400 1371 response.assert_response().element_contains(
1401 1372 '#changeset_compare_view_content .alert',
1402 1373 'This pull request cannot be displayed, because one or more'
1403 1374 ' commits no longer exist in the source repository.')
1404 1375 response.assert_response().element_contains(
1405 1376 '#update_commits',
1406 1377 'Update commits')
1407 1378
1408 1379 def test_strip_commits_and_update(
1409 1380 self, backend, pr_util, csrf_token):
1410 1381 commits = [
1411 1382 {'message': 'initial-commit'},
1412 1383 {'message': 'old-feature'},
1413 1384 {'message': 'new-feature', 'parents': ['old-feature']},
1414 1385 ]
1415 1386 pull_request = pr_util.create_pull_request(
1416 1387 commits, target_head='old-feature', source_head='new-feature',
1417 1388 revisions=['new-feature'], mergeable=True)
1418 1389 pr_id = pull_request.pull_request_id
1419 1390 target_repo_name = pull_request.target_repo.repo_name
1420 1391
1421 1392 vcs = pr_util.source_repository.scm_instance()
1422 1393 if backend.alias == 'git':
1423 1394 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1424 1395 else:
1425 1396 vcs.strip(pr_util.commit_ids['new-feature'])
1426 1397
1427 1398 url = route_path('pullrequest_update',
1428 1399 repo_name=target_repo_name,
1429 1400 pull_request_id=pr_id)
1430 1401 response = self.app.post(url,
1431 1402 params={'update_commits': 'true',
1432 1403 'csrf_token': csrf_token})
1433 1404
1434 1405 assert response.status_int == 200
1435 1406 assert json.loads(response.body) == json.loads('{"response": true, "redirect_url": null}')
1436 1407
1437 1408 # Make sure that after update, it won't raise 500 errors
1438 1409 response = self.app.get(route_path(
1439 1410 'pullrequest_show',
1440 1411 repo_name=target_repo_name,
1441 1412 pull_request_id=pr_id))
1442 1413
1443 1414 assert response.status_int == 200
1444 1415 response.assert_response().element_contains(
1445 1416 '#changeset_compare_view_content .alert strong',
1446 1417 'Missing commits')
1447 1418
1448 1419 def test_branch_is_a_link(self, pr_util):
1449 1420 pull_request = pr_util.create_pull_request()
1450 1421 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1451 1422 pull_request.target_ref = 'branch:target:abcdef1234567890'
1452 1423 Session().add(pull_request)
1453 1424 Session().commit()
1454 1425
1455 1426 response = self.app.get(route_path(
1456 1427 'pullrequest_show',
1457 1428 repo_name=pull_request.target_repo.scm_instance().name,
1458 1429 pull_request_id=pull_request.pull_request_id))
1459 1430 assert response.status_int == 200
1460 1431
1461 1432 source = response.assert_response().get_element('.pr-source-info')
1462 1433 source_parent = source.getparent()
1463 1434 assert len(source_parent) == 1
1464 1435
1465 1436 target = response.assert_response().get_element('.pr-target-info')
1466 1437 target_parent = target.getparent()
1467 1438 assert len(target_parent) == 1
1468 1439
1469 1440 expected_origin_link = route_path(
1470 1441 'repo_commits',
1471 1442 repo_name=pull_request.source_repo.scm_instance().name,
1472 1443 params=dict(branch='origin'))
1473 1444 expected_target_link = route_path(
1474 1445 'repo_commits',
1475 1446 repo_name=pull_request.target_repo.scm_instance().name,
1476 1447 params=dict(branch='target'))
1477 1448 assert source_parent.attrib['href'] == expected_origin_link
1478 1449 assert target_parent.attrib['href'] == expected_target_link
1479 1450
1480 1451 def test_bookmark_is_not_a_link(self, pr_util):
1481 1452 pull_request = pr_util.create_pull_request()
1482 1453 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1483 1454 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1484 1455 Session().add(pull_request)
1485 1456 Session().commit()
1486 1457
1487 1458 response = self.app.get(route_path(
1488 1459 'pullrequest_show',
1489 1460 repo_name=pull_request.target_repo.scm_instance().name,
1490 1461 pull_request_id=pull_request.pull_request_id))
1491 1462 assert response.status_int == 200
1492 1463
1493 1464 source = response.assert_response().get_element('.pr-source-info')
1494 1465 assert source.text.strip() == 'bookmark:origin'
1495 1466 assert source.getparent().attrib.get('href') is None
1496 1467
1497 1468 target = response.assert_response().get_element('.pr-target-info')
1498 1469 assert target.text.strip() == 'bookmark:target'
1499 1470 assert target.getparent().attrib.get('href') is None
1500 1471
1501 1472 def test_tag_is_not_a_link(self, pr_util):
1502 1473 pull_request = pr_util.create_pull_request()
1503 1474 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1504 1475 pull_request.target_ref = 'tag:target:abcdef1234567890'
1505 1476 Session().add(pull_request)
1506 1477 Session().commit()
1507 1478
1508 1479 response = self.app.get(route_path(
1509 1480 'pullrequest_show',
1510 1481 repo_name=pull_request.target_repo.scm_instance().name,
1511 1482 pull_request_id=pull_request.pull_request_id))
1512 1483 assert response.status_int == 200
1513 1484
1514 1485 source = response.assert_response().get_element('.pr-source-info')
1515 1486 assert source.text.strip() == 'tag:origin'
1516 1487 assert source.getparent().attrib.get('href') is None
1517 1488
1518 1489 target = response.assert_response().get_element('.pr-target-info')
1519 1490 assert target.text.strip() == 'tag:target'
1520 1491 assert target.getparent().attrib.get('href') is None
1521 1492
1522 1493 @pytest.mark.parametrize('mergeable', [True, False])
1523 1494 def test_shadow_repository_link(
1524 1495 self, mergeable, pr_util, http_host_only_stub):
1525 1496 """
1526 1497 Check that the pull request summary page displays a link to the shadow
1527 1498 repository if the pull request is mergeable. If it is not mergeable
1528 1499 the link should not be displayed.
1529 1500 """
1530 1501 pull_request = pr_util.create_pull_request(
1531 1502 mergeable=mergeable, enable_notifications=False)
1532 1503 target_repo = pull_request.target_repo.scm_instance()
1533 1504 pr_id = pull_request.pull_request_id
1534 1505 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1535 1506 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1536 1507
1537 1508 response = self.app.get(route_path(
1538 1509 'pullrequest_show',
1539 1510 repo_name=target_repo.name,
1540 1511 pull_request_id=pr_id))
1541 1512
1542 1513 if mergeable:
1543 1514 response.assert_response().element_value_contains(
1544 1515 'input.pr-mergeinfo', shadow_url)
1545 1516 response.assert_response().element_value_contains(
1546 1517 'input.pr-mergeinfo ', 'pr-merge')
1547 1518 else:
1548 1519 response.assert_response().no_element_exists('.pr-mergeinfo')
1549 1520
1550 1521
1551 1522 @pytest.mark.usefixtures('app')
1552 1523 @pytest.mark.backends("git", "hg")
1553 1524 class TestPullrequestsControllerDelete(object):
1554 1525 def test_pull_request_delete_button_permissions_admin(
1555 1526 self, autologin_user, user_admin, pr_util):
1556 1527 pull_request = pr_util.create_pull_request(
1557 1528 author=user_admin.username, enable_notifications=False)
1558 1529
1559 1530 response = self.app.get(route_path(
1560 1531 'pullrequest_show',
1561 1532 repo_name=pull_request.target_repo.scm_instance().name,
1562 1533 pull_request_id=pull_request.pull_request_id))
1563 1534
1564 1535 response.mustcontain('id="delete_pullrequest"')
1565 1536 response.mustcontain('Confirm to delete this pull request')
1566 1537
1567 1538 def test_pull_request_delete_button_permissions_owner(
1568 1539 self, autologin_regular_user, user_regular, pr_util):
1569 1540 pull_request = pr_util.create_pull_request(
1570 1541 author=user_regular.username, enable_notifications=False)
1571 1542
1572 1543 response = self.app.get(route_path(
1573 1544 'pullrequest_show',
1574 1545 repo_name=pull_request.target_repo.scm_instance().name,
1575 1546 pull_request_id=pull_request.pull_request_id))
1576 1547
1577 1548 response.mustcontain('id="delete_pullrequest"')
1578 1549 response.mustcontain('Confirm to delete this pull request')
1579 1550
1580 1551 def test_pull_request_delete_button_permissions_forbidden(
1581 1552 self, autologin_regular_user, user_regular, user_admin, pr_util):
1582 1553 pull_request = pr_util.create_pull_request(
1583 1554 author=user_admin.username, enable_notifications=False)
1584 1555
1585 1556 response = self.app.get(route_path(
1586 1557 'pullrequest_show',
1587 1558 repo_name=pull_request.target_repo.scm_instance().name,
1588 1559 pull_request_id=pull_request.pull_request_id))
1589 1560 response.mustcontain(no=['id="delete_pullrequest"'])
1590 1561 response.mustcontain(no=['Confirm to delete this pull request'])
1591 1562
1592 1563 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1593 1564 self, autologin_regular_user, user_regular, user_admin, pr_util,
1594 1565 user_util):
1595 1566
1596 1567 pull_request = pr_util.create_pull_request(
1597 1568 author=user_admin.username, enable_notifications=False)
1598 1569
1599 1570 user_util.grant_user_permission_to_repo(
1600 1571 pull_request.target_repo, user_regular,
1601 1572 'repository.write')
1602 1573
1603 1574 response = self.app.get(route_path(
1604 1575 'pullrequest_show',
1605 1576 repo_name=pull_request.target_repo.scm_instance().name,
1606 1577 pull_request_id=pull_request.pull_request_id))
1607 1578
1608 1579 response.mustcontain('id="open_edit_pullrequest"')
1609 1580 response.mustcontain('id="delete_pullrequest"')
1610 1581 response.mustcontain(no=['Confirm to delete this pull request'])
1611 1582
1612 1583 def test_delete_comment_returns_404_if_comment_does_not_exist(
1613 1584 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1614 1585
1615 1586 pull_request = pr_util.create_pull_request(
1616 1587 author=user_admin.username, enable_notifications=False)
1617 1588
1618 1589 self.app.post(
1619 1590 route_path(
1620 1591 'pullrequest_comment_delete',
1621 1592 repo_name=pull_request.target_repo.scm_instance().name,
1622 1593 pull_request_id=pull_request.pull_request_id,
1623 1594 comment_id=1024404),
1624 1595 extra_environ=xhr_header,
1625 1596 params={'csrf_token': csrf_token},
1626 1597 status=404
1627 1598 )
1628 1599
1629 1600 def test_delete_comment(
1630 1601 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1631 1602
1632 1603 pull_request = pr_util.create_pull_request(
1633 1604 author=user_admin.username, enable_notifications=False)
1634 1605 comment = pr_util.create_comment()
1635 1606 comment_id = comment.comment_id
1636 1607
1637 1608 response = self.app.post(
1638 1609 route_path(
1639 1610 'pullrequest_comment_delete',
1640 1611 repo_name=pull_request.target_repo.scm_instance().name,
1641 1612 pull_request_id=pull_request.pull_request_id,
1642 1613 comment_id=comment_id),
1643 1614 extra_environ=xhr_header,
1644 1615 params={'csrf_token': csrf_token},
1645 1616 status=200
1646 1617 )
1647 1618 assert response.text == 'true'
1648 1619
1649 1620 @pytest.mark.parametrize('url_type', [
1650 1621 'pullrequest_new',
1651 1622 'pullrequest_create',
1652 1623 'pullrequest_update',
1653 1624 'pullrequest_merge',
1654 1625 ])
1655 1626 def test_pull_request_is_forbidden_on_archived_repo(
1656 1627 self, autologin_user, backend, xhr_header, user_util, url_type):
1657 1628
1658 1629 # create a temporary repo
1659 1630 source = user_util.create_repo(repo_type=backend.alias)
1660 1631 repo_name = source.repo_name
1661 1632 repo = Repository.get_by_repo_name(repo_name)
1662 1633 repo.archived = True
1663 1634 Session().commit()
1664 1635
1665 1636 response = self.app.get(
1666 1637 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1667 1638
1668 1639 msg = 'Action not supported for archived repository.'
1669 1640 assert_session_flash(response, msg)
1670 1641
1671 1642
1672 1643 def assert_pull_request_status(pull_request, expected_status):
1673 1644 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1674 1645 assert status == expected_status
1675 1646
1676 1647
1677 1648 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1678 1649 @pytest.mark.usefixtures("autologin_user")
1679 1650 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1680 1651 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,233 +1,212 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib.utils2 import str2bool
24 24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 25 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests import (
28 28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
29 29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.routes import route_path
30 31
31 32 fixture = Fixture()
32 33
33 34
34 def route_path(name, params=None, **kwargs):
35 import urllib.request
36 import urllib.parse
37 import urllib.error
38
39 base_url = {
40 'edit_repo': '/{repo_name}/settings',
41 'edit_repo_advanced': '/{repo_name}/settings/advanced',
42 'edit_repo_caches': '/{repo_name}/settings/caches',
43 'edit_repo_perms': '/{repo_name}/settings/permissions',
44 'edit_repo_vcs': '/{repo_name}/settings/vcs',
45 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
46 'edit_repo_fields': '/{repo_name}/settings/fields',
47 'edit_repo_remote': '/{repo_name}/settings/remote',
48 'edit_repo_statistics': '/{repo_name}/settings/statistics',
49 }[name].format(**kwargs)
50
51 if params:
52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 return base_url
54
55
56 35 def _get_permission_for_user(user, repo):
57 36 perm = UserRepoToPerm.query()\
58 37 .filter(UserRepoToPerm.repository ==
59 38 Repository.get_by_repo_name(repo))\
60 39 .filter(UserRepoToPerm.user == User.get_by_username(user))\
61 40 .all()
62 41 return perm
63 42
64 43
65 44 @pytest.mark.usefixtures('autologin_user', 'app')
66 45 class TestAdminRepoSettings(object):
67 46 @pytest.mark.parametrize('urlname', [
68 47 'edit_repo',
69 48 'edit_repo_caches',
70 49 'edit_repo_perms',
71 50 'edit_repo_advanced',
72 51 'edit_repo_vcs',
73 52 'edit_repo_issuetracker',
74 53 'edit_repo_fields',
75 54 'edit_repo_remote',
76 55 'edit_repo_statistics',
77 56 ])
78 57 def test_show_page(self, urlname, app, backend):
79 58 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
80 59
81 60 def test_edit_accessible_when_missing_requirements(
82 61 self, backend_hg, autologin_user):
83 62 scm_patcher = mock.patch.object(
84 63 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
85 64 with scm_patcher:
86 65 self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
87 66
88 67 @pytest.mark.parametrize('update_settings', [
89 68 {'repo_description': 'alter-desc'},
90 69 {'repo_owner': TEST_USER_REGULAR_LOGIN},
91 70 {'repo_private': 'true'},
92 71 {'repo_enable_locking': 'true'},
93 72 {'repo_enable_downloads': 'true'},
94 73 ])
95 74 def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
96 75 repo = user_util.create_repo(repo_type=backend.alias)
97 76 repo_name = repo.repo_name
98 77
99 78 params = fixture._get_repo_create_params(
100 79 csrf_token=csrf_token,
101 80 repo_name=repo_name,
102 81 repo_type=backend.alias,
103 82 repo_owner=TEST_USER_ADMIN_LOGIN,
104 83 repo_description='DESC',
105 84
106 85 repo_private='false',
107 86 repo_enable_locking='false',
108 87 repo_enable_downloads='false')
109 88 params.update(update_settings)
110 89 self.app.post(
111 90 route_path('edit_repo', repo_name=repo_name),
112 91 params=params, status=302)
113 92
114 93 repo = Repository.get_by_repo_name(repo_name)
115 94 assert repo.user.username == \
116 95 update_settings.get('repo_owner', repo.user.username)
117 96
118 97 assert repo.description == \
119 98 update_settings.get('repo_description', repo.description)
120 99
121 100 assert repo.private == \
122 101 str2bool(update_settings.get(
123 102 'repo_private', repo.private))
124 103
125 104 assert repo.enable_locking == \
126 105 str2bool(update_settings.get(
127 106 'repo_enable_locking', repo.enable_locking))
128 107
129 108 assert repo.enable_downloads == \
130 109 str2bool(update_settings.get(
131 110 'repo_enable_downloads', repo.enable_downloads))
132 111
133 112 def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
134 113 repo = user_util.create_repo(repo_type=backend.alias)
135 114 repo_name = repo.repo_name
136 115
137 116 repo_group = user_util.create_repo_group()
138 117 repo_group_name = repo_group.group_name
139 118 new_name = repo_group_name + '_' + repo_name
140 119
141 120 params = fixture._get_repo_create_params(
142 121 csrf_token=csrf_token,
143 122 repo_name=new_name,
144 123 repo_type=backend.alias,
145 124 repo_owner=TEST_USER_ADMIN_LOGIN,
146 125 repo_description='DESC',
147 126 repo_private='false',
148 127 repo_enable_locking='false',
149 128 repo_enable_downloads='false')
150 129 self.app.post(
151 130 route_path('edit_repo', repo_name=repo_name),
152 131 params=params, status=302)
153 132 repo = Repository.get_by_repo_name(new_name)
154 133 assert repo.repo_name == new_name
155 134
156 135 def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
157 136 repo = user_util.create_repo(repo_type=backend.alias)
158 137 repo_name = repo.repo_name
159 138
160 139 repo_group = user_util.create_repo_group()
161 140 repo_group_name = repo_group.group_name
162 141 repo_group_id = repo_group.group_id
163 142
164 143 new_name = repo_group_name + '/' + repo_name
165 144 params = fixture._get_repo_create_params(
166 145 csrf_token=csrf_token,
167 146 repo_name=repo_name,
168 147 repo_type=backend.alias,
169 148 repo_owner=TEST_USER_ADMIN_LOGIN,
170 149 repo_description='DESC',
171 150 repo_group=repo_group_id,
172 151 repo_private='false',
173 152 repo_enable_locking='false',
174 153 repo_enable_downloads='false')
175 154 self.app.post(
176 155 route_path('edit_repo', repo_name=repo_name),
177 156 params=params, status=302)
178 157 repo = Repository.get_by_repo_name(new_name)
179 158 assert repo.repo_name == new_name
180 159
181 160 def test_set_private_flag_sets_default_user_permissions_to_none(
182 161 self, autologin_user, backend, csrf_token):
183 162
184 163 # initially repository perm should be read
185 164 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
186 165 assert len(perm) == 1
187 166 assert perm[0].permission.permission_name == 'repository.read'
188 167 assert not backend.repo.private
189 168
190 169 response = self.app.post(
191 170 route_path('edit_repo', repo_name=backend.repo_name),
192 171 params=fixture._get_repo_create_params(
193 172 repo_private='true',
194 173 repo_name=backend.repo_name,
195 174 repo_type=backend.alias,
196 175 repo_owner=TEST_USER_ADMIN_LOGIN,
197 176 csrf_token=csrf_token), status=302)
198 177
199 178 assert_session_flash(
200 179 response,
201 180 msg='Repository `%s` updated successfully' % (backend.repo_name))
202 181
203 182 repo = Repository.get_by_repo_name(backend.repo_name)
204 183 assert repo.private is True
205 184
206 185 # now the repo default permission should be None
207 186 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
208 187 assert len(perm) == 1
209 188 assert perm[0].permission.permission_name == 'repository.none'
210 189
211 190 response = self.app.post(
212 191 route_path('edit_repo', repo_name=backend.repo_name),
213 192 params=fixture._get_repo_create_params(
214 193 repo_private='false',
215 194 repo_name=backend.repo_name,
216 195 repo_type=backend.alias,
217 196 repo_owner=TEST_USER_ADMIN_LOGIN,
218 197 csrf_token=csrf_token), status=302)
219 198
220 199 assert_session_flash(
221 200 response,
222 201 msg='Repository `%s` updated successfully' % (backend.repo_name))
223 202 assert backend.repo.private is False
224 203
225 204 # we turn off private now the repo default permission should stay None
226 205 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
227 206 assert len(perm) == 1
228 207 assert perm[0].permission.permission_name == 'repository.none'
229 208
230 209 # update this permission back
231 210 perm[0].permission = Permission.get_by_key('repository.read')
232 211 Session().add(perm[0])
233 212 Session().commit()
@@ -1,174 +1,153 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.lib.str_utils import safe_str
23 23 from rhodecode.model.db import Repository
24 24 from rhodecode.model.repo import RepoModel
25 25 from rhodecode.tests import (
26 26 HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
27 27 from rhodecode.tests.fixture import Fixture
28 28 from rhodecode.tests.utils import repo_on_filesystem
29 from rhodecode.tests.routes import route_path
29 30
30 31 fixture = Fixture()
31 32
32 33
33 def route_path(name, params=None, **kwargs):
34 import urllib.request
35 import urllib.parse
36 import urllib.error
37
38 base_url = {
39 'repo_summary_explicit': '/{repo_name}/summary',
40 'repo_summary': '/{repo_name}',
41 'edit_repo_advanced': '/{repo_name}/settings/advanced',
42 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
43 'edit_repo_advanced_archive': '/{repo_name}/settings/advanced/archive',
44 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
45 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
46 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
47
48 }[name].format(**kwargs)
49
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 return base_url
53
54
55 34 @pytest.mark.usefixtures('autologin_user', 'app')
56 35 class TestAdminRepoSettingsAdvanced(object):
57 36
58 37 def test_set_repo_fork_has_no_self_id(self, autologin_user, backend):
59 38 repo = backend.repo
60 39 response = self.app.get(
61 40 route_path('edit_repo_advanced', repo_name=backend.repo_name))
62 41 opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
63 42 response.mustcontain(no=[opt])
64 43
65 44 def test_set_fork_of_target_repo(
66 45 self, autologin_user, backend, csrf_token):
67 46 target_repo = 'target_%s' % backend.alias
68 47 fixture.create_repo(target_repo, repo_type=backend.alias)
69 48 repo2 = Repository.get_by_repo_name(target_repo)
70 49 response = self.app.post(
71 50 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
72 51 params={'id_fork_of': repo2.repo_id,
73 52 'csrf_token': csrf_token})
74 53 repo = Repository.get_by_repo_name(backend.repo_name)
75 54 repo2 = Repository.get_by_repo_name(target_repo)
76 55 assert_session_flash(
77 56 response,
78 57 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
79 58
80 59 assert repo.fork == repo2
81 60 response = response.follow()
82 61 # check if given repo is selected
83 62
84 63 opt = 'This repository is a fork of <a href="%s">%s</a>' % (
85 64 route_path('repo_summary', repo_name=repo2.repo_name),
86 65 repo2.repo_name)
87 66
88 67 response.mustcontain(opt)
89 68
90 69 fixture.destroy_repo(target_repo, forks='detach')
91 70
92 71 @pytest.mark.backends("hg", "git")
93 72 def test_set_fork_of_other_type_repo(
94 73 self, autologin_user, backend, csrf_token):
95 74 TARGET_REPO_MAP = {
96 75 'git': {
97 76 'type': 'hg',
98 77 'repo_name': HG_REPO},
99 78 'hg': {
100 79 'type': 'git',
101 80 'repo_name': GIT_REPO},
102 81 }
103 82 target_repo = TARGET_REPO_MAP[backend.alias]
104 83
105 84 repo2 = Repository.get_by_repo_name(target_repo['repo_name'])
106 85 response = self.app.post(
107 86 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
108 87 params={'id_fork_of': repo2.repo_id,
109 88 'csrf_token': csrf_token})
110 89 assert_session_flash(
111 90 response,
112 91 'Cannot set repository as fork of repository with other type')
113 92
114 93 def test_set_fork_of_none(self, autologin_user, backend, csrf_token):
115 94 # mark it as None
116 95 response = self.app.post(
117 96 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
118 97 params={'id_fork_of': None,
119 98 'csrf_token': csrf_token})
120 99 assert_session_flash(
121 100 response,
122 101 'Marked repo %s as fork of %s'
123 102 % (backend.repo_name, "Nothing"))
124 103 assert backend.repo.fork is None
125 104
126 105 def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token):
127 106 repo = Repository.get_by_repo_name(backend.repo_name)
128 107 response = self.app.post(
129 108 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
130 109 params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token})
131 110 assert_session_flash(
132 111 response, 'An error occurred during this operation')
133 112
134 113 @pytest.mark.parametrize(
135 114 "suffix",
136 115 ['', u'ąęł' , '123'],
137 116 ids=no_newline_id_generator)
138 117 def test_advanced_repo_delete(self, autologin_user, backend, suffix, csrf_token):
139 118 repo = backend.create_repo(name_suffix=suffix)
140 119 repo_name = repo.repo_name
141 120 repo_name_str = safe_str(repo.repo_name)
142 121
143 122 response = self.app.post(
144 123 route_path('edit_repo_advanced_delete', repo_name=repo_name_str),
145 124 params={'csrf_token': csrf_token})
146 125 assert_session_flash(response,
147 126 u'Deleted repository `{}`'.format(repo_name))
148 127 response.follow()
149 128
150 129 # check if repo was deleted from db
151 130 assert RepoModel().get_by_repo_name(repo_name) is None
152 131 assert not repo_on_filesystem(repo_name_str)
153 132
154 133 @pytest.mark.parametrize(
155 134 "suffix",
156 135 ['', u'ąęł' , '123'],
157 136 ids=no_newline_id_generator)
158 137 def test_advanced_repo_archive(self, autologin_user, backend, suffix, csrf_token):
159 138 repo = backend.create_repo(name_suffix=suffix)
160 139 repo_name = repo.repo_name
161 140 repo_name_str = safe_str(repo.repo_name)
162 141
163 142 response = self.app.post(
164 143 route_path('edit_repo_advanced_archive', repo_name=repo_name_str),
165 144 params={'csrf_token': csrf_token})
166 145
167 146 assert_session_flash(response,
168 147 u'Archived repository `{}`'.format(repo_name))
169 148
170 149 response = self.app.get(route_path('repo_summary', repo_name=repo_name_str))
171 150 response.mustcontain('This repository has been archived. It is now read-only.')
172 151
173 152 # check if repo was deleted from db
174 153 assert RepoModel().get_by_repo_name(repo_name).archived is True
@@ -1,525 +1,508 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import re
21 21
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
26 26 from rhodecode.lib import helpers as h
27 27 from collections import OrderedDict
28 28 from rhodecode.lib.utils2 import AttributeDict, safe_str
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.model.db import Repository
31 31 from rhodecode.model.meta import Session
32 32 from rhodecode.model.repo import RepoModel
33 33 from rhodecode.model.scm import ScmModel
34 34 from rhodecode.tests import assert_session_flash
35 35 from rhodecode.tests.fixture import Fixture
36 36 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
37 from rhodecode.tests.routes import route_path
37 38
38 39
39 40 fixture = Fixture()
40 41
41 42
42 def route_path(name, params=None, **kwargs):
43 import urllib.request
44 import urllib.parse
45 import urllib.error
46
47 base_url = {
48 'repo_summary': '/{repo_name}',
49 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
50 'repo_refs_data': '/{repo_name}/refs-data',
51 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog',
52 'repo_creating_check': '/{repo_name}/repo_creating_check',
53 }[name].format(**kwargs)
54
55 if params:
56 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
57 return base_url
58
59
60 43 def assert_clone_url(response, server, repo, disabled=False):
61 44
62 45 response.mustcontain(
63 46 '<input type="text" class="input-monospace clone_url_input" '
64 47 '{disabled}readonly="readonly" '
65 48 'value="http://test_admin@{server}/{repo}"/>'.format(
66 49 server=server, repo=repo, disabled='disabled ' if disabled else ' ')
67 50 )
68 51
69 52
70 53 @pytest.mark.usefixtures('app')
71 54 class TestSummaryView(object):
72 55 def test_index(self, autologin_user, backend, http_host_only_stub):
73 56 repo_id = backend.repo.repo_id
74 57 repo_name = backend.repo_name
75 58 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
76 59 return_value=False):
77 60 response = self.app.get(
78 61 route_path('repo_summary', repo_name=repo_name))
79 62
80 63 # repo type
81 64 response.mustcontain(
82 65 '<i class="icon-%s">' % (backend.alias, )
83 66 )
84 67 # public/private
85 68 response.mustcontain(
86 69 """<i class="icon-unlock-alt">"""
87 70 )
88 71
89 72 # clone url...
90 73 assert_clone_url(response, http_host_only_stub, repo_name)
91 74 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
92 75
93 76 def test_index_svn_without_proxy(
94 77 self, autologin_user, backend_svn, http_host_only_stub):
95 78 repo_id = backend_svn.repo.repo_id
96 79 repo_name = backend_svn.repo_name
97 80 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
98 81 # clone url...
99 82
100 83 assert_clone_url(response, http_host_only_stub, repo_name, disabled=True)
101 84 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id), disabled=True)
102 85
103 86 def test_index_with_trailing_slash(
104 87 self, autologin_user, backend, http_host_only_stub):
105 88
106 89 repo_id = backend.repo.repo_id
107 90 repo_name = backend.repo_name
108 91 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
109 92 return_value=False):
110 93 response = self.app.get(
111 94 route_path('repo_summary', repo_name=repo_name) + '/',
112 95 status=200)
113 96
114 97 # clone url...
115 98 assert_clone_url(response, http_host_only_stub, repo_name)
116 99 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
117 100
118 101 def test_index_by_id(self, autologin_user, backend):
119 102 repo_id = backend.repo.repo_id
120 103 response = self.app.get(
121 104 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
122 105
123 106 # repo type
124 107 response.mustcontain(
125 108 '<i class="icon-%s">' % (backend.alias, )
126 109 )
127 110 # public/private
128 111 response.mustcontain(
129 112 """<i class="icon-unlock-alt">"""
130 113 )
131 114
132 115 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
133 116 fixture.create_repo(name='repo_1')
134 117 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
135 118
136 119 try:
137 120 response.mustcontain("repo_1")
138 121 finally:
139 122 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
140 123 Session().commit()
141 124
142 125 def test_index_with_anonymous_access_disabled(
143 126 self, backend, disable_anonymous_user):
144 127 response = self.app.get(
145 128 route_path('repo_summary', repo_name=backend.repo_name), status=302)
146 129 assert 'login' in response.location
147 130
148 131 def _enable_stats(self, repo):
149 132 r = Repository.get_by_repo_name(repo)
150 133 r.enable_statistics = True
151 134 Session().add(r)
152 135 Session().commit()
153 136
154 137 expected_trending = {
155 138 'hg': {
156 139 "py": {"count": 68, "desc": ["Python"]},
157 140 "rst": {"count": 16, "desc": ["Rst"]},
158 141 "css": {"count": 2, "desc": ["Css"]},
159 142 "sh": {"count": 2, "desc": ["Bash"]},
160 143 "bat": {"count": 1, "desc": ["Batch"]},
161 144 "cfg": {"count": 1, "desc": ["Ini"]},
162 145 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
163 146 "ini": {"count": 1, "desc": ["Ini"]},
164 147 "js": {"count": 1, "desc": ["Javascript"]},
165 148 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
166 149 },
167 150 'git': {
168 151 "py": {"count": 68, "desc": ["Python"]},
169 152 "rst": {"count": 16, "desc": ["Rst"]},
170 153 "css": {"count": 2, "desc": ["Css"]},
171 154 "sh": {"count": 2, "desc": ["Bash"]},
172 155 "bat": {"count": 1, "desc": ["Batch"]},
173 156 "cfg": {"count": 1, "desc": ["Ini"]},
174 157 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
175 158 "ini": {"count": 1, "desc": ["Ini"]},
176 159 "js": {"count": 1, "desc": ["Javascript"]},
177 160 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
178 161 },
179 162 'svn': {
180 163 "py": {"count": 75, "desc": ["Python"]},
181 164 "rst": {"count": 16, "desc": ["Rst"]},
182 165 "html": {"count": 11, "desc": ["EvoqueHtml", "Html"]},
183 166 "css": {"count": 2, "desc": ["Css"]},
184 167 "bat": {"count": 1, "desc": ["Batch"]},
185 168 "cfg": {"count": 1, "desc": ["Ini"]},
186 169 "ini": {"count": 1, "desc": ["Ini"]},
187 170 "js": {"count": 1, "desc": ["Javascript"]},
188 171 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]},
189 172 "sh": {"count": 1, "desc": ["Bash"]}
190 173 },
191 174 }
192 175
193 176 def test_repo_stats(self, autologin_user, backend, xhr_header):
194 177 response = self.app.get(
195 178 route_path(
196 179 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
197 180 extra_environ=xhr_header,
198 181 status=200)
199 182 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
200 183
201 184 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
202 185 repo_name = backend.repo_name
203 186
204 187 # codes stats
205 188 self._enable_stats(repo_name)
206 189 ScmModel().mark_for_invalidation(repo_name)
207 190
208 191 response = self.app.get(
209 192 route_path(
210 193 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
211 194 extra_environ=xhr_header,
212 195 status=200)
213 196
214 197 expected_data = self.expected_trending[backend.alias]
215 198 returned_stats = response.json['code_stats']
216 199 for k, v in expected_data.items():
217 200 assert v == returned_stats[k]
218 201
219 202 def test_repo_refs_data(self, backend):
220 203 response = self.app.get(
221 204 route_path('repo_refs_data', repo_name=backend.repo_name),
222 205 status=200)
223 206
224 207 # Ensure that there is the correct amount of items in the result
225 208 repo = backend.repo.scm_instance()
226 209 data = response.json['results']
227 210 items = sum(len(section['children']) for section in data)
228 211 repo_refs = len(repo.branches) + len(repo.tags) + len(repo.bookmarks)
229 212 assert items == repo_refs
230 213
231 214 def test_index_shows_missing_requirements_message(
232 215 self, backend, autologin_user):
233 216 repo_name = backend.repo_name
234 217 scm_patcher = mock.patch.object(
235 218 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
236 219
237 220 with scm_patcher:
238 221 response = self.app.get(
239 222 route_path('repo_summary', repo_name=repo_name))
240 223 assert_response = response.assert_response()
241 224 assert_response.element_contains(
242 225 '.main .alert-warning strong', 'Missing requirements')
243 226 assert_response.element_contains(
244 227 '.main .alert-warning',
245 228 'Commits cannot be displayed, because this repository '
246 229 'uses one or more extensions, which was not enabled.')
247 230
248 231 def test_missing_requirements_page_does_not_contains_switch_to(
249 232 self, autologin_user, backend):
250 233 repo_name = backend.repo_name
251 234 scm_patcher = mock.patch.object(
252 235 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
253 236
254 237 with scm_patcher:
255 238 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
256 239 response.mustcontain(no='Switch To')
257 240
258 241
259 242 @pytest.mark.usefixtures('app')
260 243 class TestRepoLocation(object):
261 244
262 245 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
263 246 def test_missing_filesystem_repo(
264 247 self, autologin_user, backend, suffix, csrf_token):
265 248 repo = backend.create_repo(name_suffix=suffix)
266 249 repo_name = repo.repo_name
267 250
268 251 # delete from file system
269 252 RepoModel()._delete_filesystem_repo(repo)
270 253
271 254 # test if the repo is still in the database
272 255 new_repo = RepoModel().get_by_repo_name(repo_name)
273 256 assert new_repo.repo_name == repo_name
274 257
275 258 # check if repo is not in the filesystem
276 259 assert not repo_on_filesystem(repo_name)
277 260
278 261 response = self.app.get(
279 262 route_path('repo_summary', repo_name=safe_str(repo_name)), status=302)
280 263
281 264 msg = f'The repository `{repo_name}` cannot be loaded in filesystem. ' \
282 265 f'Please check if it exist, or is not damaged.'
283 266 assert_session_flash(response, msg)
284 267
285 268 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
286 269 def test_missing_filesystem_repo_on_repo_check(
287 270 self, autologin_user, backend, suffix, csrf_token):
288 271 repo = backend.create_repo(name_suffix=suffix)
289 272 repo_name = repo.repo_name
290 273
291 274 # delete from file system
292 275 RepoModel()._delete_filesystem_repo(repo)
293 276
294 277 # test if the repo is still in the database
295 278 new_repo = RepoModel().get_by_repo_name(repo_name)
296 279 assert new_repo.repo_name == repo_name
297 280
298 281 # check if repo is not in the filesystem
299 282 assert not repo_on_filesystem(repo_name)
300 283
301 284 # flush the session
302 285 self.app.get(
303 286 route_path('repo_summary', repo_name=safe_str(repo_name)),
304 287 status=302)
305 288
306 289 response = self.app.get(
307 290 route_path('repo_creating_check', repo_name=safe_str(repo_name)),
308 291 status=200)
309 292 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
310 293 'Please check if it exist, or is not damaged.' % repo_name
311 294 assert_session_flash(response, msg )
312 295
313 296
314 297 @pytest.fixture()
315 298 def summary_view(context_stub, request_stub, user_util):
316 299 """
317 300 Bootstrap view to test the view functions
318 301 """
319 302 request_stub.matched_route = AttributeDict(name='test_view')
320 303
321 304 request_stub.user = user_util.create_user().AuthUser()
322 305 request_stub.db_repo = user_util.create_repo()
323 306
324 307 view = RepoSummaryView(context=context_stub, request=request_stub)
325 308 return view
326 309
327 310
328 311 @pytest.mark.usefixtures('app')
329 312 class TestCreateReferenceData(object):
330 313
331 314 @pytest.fixture()
332 315 def example_refs(self):
333 316 section_1_refs = OrderedDict((('a', 'a_id'), ('b', 'b_id')))
334 317 example_refs = [
335 318 ('section_1', section_1_refs, 't1'),
336 319 ('section_2', {'c': 'c_id'}, 't2'),
337 320 ]
338 321 return example_refs
339 322
340 323 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
341 324 repo = mock.Mock()
342 325 repo.name = 'test-repo'
343 326 repo.alias = 'git'
344 327 full_repo_name = 'pytest-repo-group/' + repo.name
345 328
346 329 result = summary_view._create_reference_data(
347 330 repo, full_repo_name, example_refs)
348 331
349 332 expected_files_url = '/{}/files/'.format(full_repo_name)
350 333 expected_result = [
351 334 {
352 335 'children': [
353 336 {
354 337 'id': 'a', 'idx': 0, 'raw_id': 'a_id', 'text': 'a', 'type': 't1',
355 338 'files_url': expected_files_url + 'a/?at=a',
356 339 },
357 340 {
358 341 'id': 'b', 'idx': 0, 'raw_id': 'b_id', 'text': 'b', 'type': 't1',
359 342 'files_url': expected_files_url + 'b/?at=b',
360 343 }
361 344 ],
362 345 'text': 'section_1'
363 346 },
364 347 {
365 348 'children': [
366 349 {
367 350 'id': 'c', 'idx': 0, 'raw_id': 'c_id', 'text': 'c', 'type': 't2',
368 351 'files_url': expected_files_url + 'c/?at=c',
369 352 }
370 353 ],
371 354 'text': 'section_2'
372 355 }]
373 356 assert result == expected_result
374 357
375 358 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
376 359 repo = mock.Mock()
377 360 repo.name = 'test-repo'
378 361 repo.alias = 'svn'
379 362 full_repo_name = 'pytest-repo-group/' + repo.name
380 363
381 364 result = summary_view._create_reference_data(
382 365 repo, full_repo_name, example_refs)
383 366
384 367 expected_files_url = '/{}/files/'.format(full_repo_name)
385 368 expected_result = [
386 369 {
387 370 'children': [
388 371 {
389 372 'id': 'a@a_id', 'idx': 0, 'raw_id': 'a_id',
390 373 'text': 'a', 'type': 't1',
391 374 'files_url': expected_files_url + 'a_id/a?at=a',
392 375 },
393 376 {
394 377 'id': 'b@b_id', 'idx': 0, 'raw_id': 'b_id',
395 378 'text': 'b', 'type': 't1',
396 379 'files_url': expected_files_url + 'b_id/b?at=b',
397 380 }
398 381 ],
399 382 'text': 'section_1'
400 383 },
401 384 {
402 385 'children': [
403 386 {
404 387 'id': 'c@c_id', 'idx': 0, 'raw_id': 'c_id',
405 388 'text': 'c', 'type': 't2',
406 389 'files_url': expected_files_url + 'c_id/c?at=c',
407 390 }
408 391 ],
409 392 'text': 'section_2'
410 393 }
411 394 ]
412 395 assert result == expected_result
413 396
414 397
415 398 class TestCreateFilesUrl(object):
416 399
417 400 def test_creates_non_svn_url(self, app, summary_view):
418 401 repo = mock.Mock()
419 402 repo.name = 'abcde'
420 403 full_repo_name = 'test-repo-group/' + repo.name
421 404 ref_name = 'branch1'
422 405 raw_id = 'deadbeef0123456789'
423 406 is_svn = False
424 407
425 408 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
426 409 result = summary_view._create_files_url(
427 410 repo, full_repo_name, ref_name, raw_id, is_svn)
428 411 url_mock.assert_called_once_with(
429 412 'repo_files', repo_name=full_repo_name, commit_id=ref_name,
430 413 f_path='', _query=dict(at=ref_name))
431 414 assert result == url_mock.return_value
432 415
433 416 def test_creates_svn_url(self, app, summary_view):
434 417 repo = mock.Mock()
435 418 repo.name = 'abcde'
436 419 full_repo_name = 'test-repo-group/' + repo.name
437 420 ref_name = 'branch1'
438 421 raw_id = 'deadbeef0123456789'
439 422 is_svn = True
440 423
441 424 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
442 425 result = summary_view._create_files_url(
443 426 repo, full_repo_name, ref_name, raw_id, is_svn)
444 427 url_mock.assert_called_once_with(
445 428 'repo_files', repo_name=full_repo_name, f_path=ref_name,
446 429 commit_id=raw_id, _query=dict(at=ref_name))
447 430 assert result == url_mock.return_value
448 431
449 432 def test_name_has_slashes(self, app, summary_view):
450 433 repo = mock.Mock()
451 434 repo.name = 'abcde'
452 435 full_repo_name = 'test-repo-group/' + repo.name
453 436 ref_name = 'branch1/branch2'
454 437 raw_id = 'deadbeef0123456789'
455 438 is_svn = False
456 439
457 440 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
458 441 result = summary_view._create_files_url(
459 442 repo, full_repo_name, ref_name, raw_id, is_svn)
460 443 url_mock.assert_called_once_with(
461 444 'repo_files', repo_name=full_repo_name, commit_id=raw_id,
462 445 f_path='', _query=dict(at=ref_name))
463 446 assert result == url_mock.return_value
464 447
465 448
466 449 class TestReferenceItems(object):
467 450 repo = mock.Mock()
468 451 repo.name = 'pytest-repo'
469 452 repo_full_name = 'pytest-repo-group/' + repo.name
470 453 ref_type = 'branch'
471 454 fake_url = '/abcde/'
472 455
473 456 @staticmethod
474 457 def _format_function(name, id_):
475 458 return 'format_function_{}_{}'.format(name, id_)
476 459
477 460 def test_creates_required_amount_of_items(self, summary_view):
478 461 amount = 100
479 462 refs = {
480 463 'ref{}'.format(i): '{0:040d}'.format(i)
481 464 for i in range(amount)
482 465 }
483 466
484 467 url_patcher = mock.patch.object(summary_view, '_create_files_url')
485 468 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
486 469 return_value=False)
487 470
488 471 with url_patcher as url_mock, svn_patcher:
489 472 result = summary_view._create_reference_items(
490 473 self.repo, self.repo_full_name, refs, self.ref_type,
491 474 self._format_function)
492 475 assert len(result) == amount
493 476 assert url_mock.call_count == amount
494 477
495 478 def test_single_item_details(self, summary_view):
496 479 ref_name = 'ref1'
497 480 ref_id = 'deadbeef'
498 481 refs = {
499 482 ref_name: ref_id
500 483 }
501 484
502 485 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
503 486 return_value=False)
504 487
505 488 url_patcher = mock.patch.object(
506 489 summary_view, '_create_files_url', return_value=self.fake_url)
507 490
508 491 with url_patcher as url_mock, svn_patcher:
509 492 result = summary_view._create_reference_items(
510 493 self.repo, self.repo_full_name, refs, self.ref_type,
511 494 self._format_function)
512 495
513 496 url_mock.assert_called_once_with(
514 497 self.repo, self.repo_full_name, ref_name, ref_id, False)
515 498 expected_result = [
516 499 {
517 500 'text': ref_name,
518 501 'id': self._format_function(ref_name, ref_id),
519 502 'raw_id': ref_id,
520 503 'idx': 0,
521 504 'type': self.ref_type,
522 505 'files_url': self.fake_url
523 506 }
524 507 ]
525 508 assert result == expected_result
@@ -1,48 +1,35 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21 from rhodecode.model.db import Repository
22
23
24 def route_path(name, params=None, **kwargs):
25 import urllib.request
26 import urllib.parse
27 import urllib.error
28
29 base_url = {
30 'tags_home': '/{repo_name}/tags',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 return base_url
22 from rhodecode.tests.routes import route_path
36 23
37 24
38 25 @pytest.mark.usefixtures('autologin_user', 'app')
39 26 class TestTagsController(object):
40 27 def test_index(self, backend):
41 28 response = self.app.get(
42 29 route_path('tags_home', repo_name=backend.repo_name))
43 30
44 31 repo = Repository.get_by_repo_name(backend.repo_name)
45 32
46 33 for commit_id, obj_name in repo.scm_instance().tags.items():
47 34 assert commit_id in response
48 35 assert obj_name in response
@@ -1,686 +1,668 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.lib import auth
24 24 from rhodecode.lib.utils2 import str2bool
25 25 from rhodecode.model.db import (
26 26 Repository, UserRepoToPerm, User)
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
29 29 from rhodecode.model.user import UserModel
30 30 from rhodecode.tests import (
31 31 login_user_session, logout_user_session,
32 32 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
33 33 from rhodecode.tests.fixture import Fixture
34 34 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.routes import route_path
35 36
36 37 fixture = Fixture()
37 38
38 39
39 def route_path(name, params=None, **kwargs):
40 import urllib.request
41 import urllib.parse
42 import urllib.error
43
44 base_url = {
45 'repo_summary': '/{repo_name}',
46 'repo_creating_check': '/{repo_name}/repo_creating_check',
47 'edit_repo': '/{repo_name}/settings',
48 'edit_repo_vcs': '/{repo_name}/settings/vcs',
49 'edit_repo_vcs_update': '/{repo_name}/settings/vcs/update',
50 'edit_repo_vcs_svn_pattern_delete': '/{repo_name}/settings/vcs/svn_pattern/delete'
51 }[name].format(**kwargs)
52
53 if params:
54 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
55 return base_url
56
57
58 40 @pytest.mark.usefixtures("app")
59 41 class TestVcsSettings(object):
60 42 FORM_DATA = {
61 43 'inherit_global_settings': False,
62 44 'hooks_changegroup_repo_size': False,
63 45 'hooks_changegroup_push_logger': False,
64 46 'hooks_outgoing_pull_logger': False,
65 47 'extensions_largefiles': False,
66 48 'extensions_evolve': False,
67 49 'phases_publish': 'False',
68 50 'rhodecode_pr_merge_enabled': False,
69 51 'rhodecode_use_outdated_comments': False,
70 52 'new_svn_branch': '',
71 53 'new_svn_tag': ''
72 54 }
73 55
74 56 @pytest.mark.skip_backends('svn')
75 57 def test_global_settings_initial_values(self, autologin_user, backend):
76 58 repo_name = backend.repo_name
77 59 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
78 60
79 61 expected_settings = (
80 62 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled',
81 63 'hooks_changegroup_repo_size', 'hooks_changegroup_push_logger',
82 64 'hooks_outgoing_pull_logger'
83 65 )
84 66 for setting in expected_settings:
85 67 self.assert_repo_value_equals_global_value(response, setting)
86 68
87 69 def test_show_settings_requires_repo_admin_permission(
88 70 self, backend, user_util, settings_util):
89 71 repo = backend.create_repo()
90 72 repo_name = repo.repo_name
91 73 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
92 74 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
93 75 login_user_session(
94 76 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
95 77 self.app.get(route_path('edit_repo_vcs', repo_name=repo_name), status=200)
96 78
97 79 def test_inherit_global_settings_flag_is_true_by_default(
98 80 self, autologin_user, backend):
99 81 repo_name = backend.repo_name
100 82 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
101 83
102 84 assert_response = response.assert_response()
103 85 element = assert_response.get_element('#inherit_global_settings')
104 86 assert element.checked
105 87
106 88 @pytest.mark.parametrize('checked_value', [True, False])
107 89 def test_inherit_global_settings_value(
108 90 self, autologin_user, backend, checked_value, settings_util):
109 91 repo = backend.create_repo()
110 92 repo_name = repo.repo_name
111 93 settings_util.create_repo_rhodecode_setting(
112 94 repo, 'inherit_vcs_settings', checked_value, 'bool')
113 95 response = self.app.get(route_path('edit_repo_vcs', repo_name=repo_name))
114 96
115 97 assert_response = response.assert_response()
116 98 element = assert_response.get_element('#inherit_global_settings')
117 99 assert element.checked == checked_value
118 100
119 101 @pytest.mark.skip_backends('svn')
120 102 def test_hooks_settings_are_created(
121 103 self, autologin_user, backend, csrf_token):
122 104 repo_name = backend.repo_name
123 105 data = self.FORM_DATA.copy()
124 106 data['csrf_token'] = csrf_token
125 107 self.app.post(
126 108 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
127 109 settings = SettingsModel(repo=repo_name)
128 110 try:
129 111 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
130 112 ui = settings.get_ui_by_section_and_key(section, key)
131 113 assert ui.ui_active is False
132 114 finally:
133 115 self._cleanup_repo_settings(settings)
134 116
135 117 def test_hooks_settings_are_not_created_for_svn(
136 118 self, autologin_user, backend_svn, csrf_token):
137 119 repo_name = backend_svn.repo_name
138 120 data = self.FORM_DATA.copy()
139 121 data['csrf_token'] = csrf_token
140 122 self.app.post(
141 123 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
142 124 settings = SettingsModel(repo=repo_name)
143 125 try:
144 126 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
145 127 ui = settings.get_ui_by_section_and_key(section, key)
146 128 assert ui is None
147 129 finally:
148 130 self._cleanup_repo_settings(settings)
149 131
150 132 @pytest.mark.skip_backends('svn')
151 133 def test_hooks_settings_are_updated(
152 134 self, autologin_user, backend, csrf_token):
153 135 repo_name = backend.repo_name
154 136 settings = SettingsModel(repo=repo_name)
155 137 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
156 138 settings.create_ui_section_value(section, '', key=key, active=True)
157 139
158 140 data = self.FORM_DATA.copy()
159 141 data['csrf_token'] = csrf_token
160 142 self.app.post(
161 143 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
162 144 try:
163 145 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
164 146 ui = settings.get_ui_by_section_and_key(section, key)
165 147 assert ui.ui_active is False
166 148 finally:
167 149 self._cleanup_repo_settings(settings)
168 150
169 151 def test_hooks_settings_are_not_updated_for_svn(
170 152 self, autologin_user, backend_svn, csrf_token):
171 153 repo_name = backend_svn.repo_name
172 154 settings = SettingsModel(repo=repo_name)
173 155 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
174 156 settings.create_ui_section_value(section, '', key=key, active=True)
175 157
176 158 data = self.FORM_DATA.copy()
177 159 data['csrf_token'] = csrf_token
178 160 self.app.post(
179 161 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
180 162 try:
181 163 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
182 164 ui = settings.get_ui_by_section_and_key(section, key)
183 165 assert ui.ui_active is True
184 166 finally:
185 167 self._cleanup_repo_settings(settings)
186 168
187 169 @pytest.mark.skip_backends('svn')
188 170 def test_pr_settings_are_created(
189 171 self, autologin_user, backend, csrf_token):
190 172 repo_name = backend.repo_name
191 173 data = self.FORM_DATA.copy()
192 174 data['csrf_token'] = csrf_token
193 175 self.app.post(
194 176 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
195 177 settings = SettingsModel(repo=repo_name)
196 178 try:
197 179 for name in VcsSettingsModel.GENERAL_SETTINGS:
198 180 setting = settings.get_setting_by_name(name)
199 181 assert setting.app_settings_value is False
200 182 finally:
201 183 self._cleanup_repo_settings(settings)
202 184
203 185 def test_pr_settings_are_not_created_for_svn(
204 186 self, autologin_user, backend_svn, csrf_token):
205 187 repo_name = backend_svn.repo_name
206 188 data = self.FORM_DATA.copy()
207 189 data['csrf_token'] = csrf_token
208 190 self.app.post(
209 191 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
210 192 settings = SettingsModel(repo=repo_name)
211 193 try:
212 194 for name in VcsSettingsModel.GENERAL_SETTINGS:
213 195 setting = settings.get_setting_by_name(name)
214 196 assert setting is None
215 197 finally:
216 198 self._cleanup_repo_settings(settings)
217 199
218 200 def test_pr_settings_creation_requires_repo_admin_permission(
219 201 self, backend, user_util, settings_util, csrf_token):
220 202 repo = backend.create_repo()
221 203 repo_name = repo.repo_name
222 204
223 205 logout_user_session(self.app, csrf_token)
224 206 session = login_user_session(
225 207 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
226 208 new_csrf_token = auth.get_csrf_token(session)
227 209
228 210 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
229 211 repo = Repository.get_by_repo_name(repo_name)
230 212 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
231 213 data = self.FORM_DATA.copy()
232 214 data['csrf_token'] = new_csrf_token
233 215 settings = SettingsModel(repo=repo_name)
234 216
235 217 try:
236 218 self.app.post(
237 219 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
238 220 status=302)
239 221 finally:
240 222 self._cleanup_repo_settings(settings)
241 223
242 224 @pytest.mark.skip_backends('svn')
243 225 def test_pr_settings_are_updated(
244 226 self, autologin_user, backend, csrf_token):
245 227 repo_name = backend.repo_name
246 228 settings = SettingsModel(repo=repo_name)
247 229 for name in VcsSettingsModel.GENERAL_SETTINGS:
248 230 settings.create_or_update_setting(name, True, 'bool')
249 231
250 232 data = self.FORM_DATA.copy()
251 233 data['csrf_token'] = csrf_token
252 234 self.app.post(
253 235 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
254 236 try:
255 237 for name in VcsSettingsModel.GENERAL_SETTINGS:
256 238 setting = settings.get_setting_by_name(name)
257 239 assert setting.app_settings_value is False
258 240 finally:
259 241 self._cleanup_repo_settings(settings)
260 242
261 243 def test_pr_settings_are_not_updated_for_svn(
262 244 self, autologin_user, backend_svn, csrf_token):
263 245 repo_name = backend_svn.repo_name
264 246 settings = SettingsModel(repo=repo_name)
265 247 for name in VcsSettingsModel.GENERAL_SETTINGS:
266 248 settings.create_or_update_setting(name, True, 'bool')
267 249
268 250 data = self.FORM_DATA.copy()
269 251 data['csrf_token'] = csrf_token
270 252 self.app.post(
271 253 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
272 254 try:
273 255 for name in VcsSettingsModel.GENERAL_SETTINGS:
274 256 setting = settings.get_setting_by_name(name)
275 257 assert setting.app_settings_value is True
276 258 finally:
277 259 self._cleanup_repo_settings(settings)
278 260
279 261 def test_svn_settings_are_created(
280 262 self, autologin_user, backend_svn, csrf_token, settings_util):
281 263 repo_name = backend_svn.repo_name
282 264 data = self.FORM_DATA.copy()
283 265 data['new_svn_tag'] = 'svn-tag'
284 266 data['new_svn_branch'] = 'svn-branch'
285 267 data['csrf_token'] = csrf_token
286 268
287 269 # Create few global settings to make sure that uniqueness validators
288 270 # are not triggered
289 271 settings_util.create_rhodecode_ui(
290 272 VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
291 273 settings_util.create_rhodecode_ui(
292 274 VcsSettingsModel.SVN_TAG_SECTION, 'svn-tag')
293 275
294 276 self.app.post(
295 277 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
296 278 settings = SettingsModel(repo=repo_name)
297 279 try:
298 280 svn_branches = settings.get_ui_by_section(
299 281 VcsSettingsModel.SVN_BRANCH_SECTION)
300 282 svn_branch_names = [b.ui_value for b in svn_branches]
301 283 svn_tags = settings.get_ui_by_section(
302 284 VcsSettingsModel.SVN_TAG_SECTION)
303 285 svn_tag_names = [b.ui_value for b in svn_tags]
304 286 assert 'svn-branch' in svn_branch_names
305 287 assert 'svn-tag' in svn_tag_names
306 288 finally:
307 289 self._cleanup_repo_settings(settings)
308 290
309 291 def test_svn_settings_are_unique(
310 292 self, autologin_user, backend_svn, csrf_token, settings_util):
311 293 repo = backend_svn.repo
312 294 repo_name = repo.repo_name
313 295 data = self.FORM_DATA.copy()
314 296 data['new_svn_tag'] = 'test_tag'
315 297 data['new_svn_branch'] = 'test_branch'
316 298 data['csrf_token'] = csrf_token
317 299 settings_util.create_repo_rhodecode_ui(
318 300 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch')
319 301 settings_util.create_repo_rhodecode_ui(
320 302 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag')
321 303
322 304 response = self.app.post(
323 305 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=200)
324 306 response.mustcontain('Pattern already exists')
325 307
326 308 def test_svn_settings_with_empty_values_are_not_created(
327 309 self, autologin_user, backend_svn, csrf_token):
328 310 repo_name = backend_svn.repo_name
329 311 data = self.FORM_DATA.copy()
330 312 data['csrf_token'] = csrf_token
331 313 self.app.post(
332 314 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
333 315 settings = SettingsModel(repo=repo_name)
334 316 try:
335 317 svn_branches = settings.get_ui_by_section(
336 318 VcsSettingsModel.SVN_BRANCH_SECTION)
337 319 svn_tags = settings.get_ui_by_section(
338 320 VcsSettingsModel.SVN_TAG_SECTION)
339 321 assert len(svn_branches) == 0
340 322 assert len(svn_tags) == 0
341 323 finally:
342 324 self._cleanup_repo_settings(settings)
343 325
344 326 def test_svn_settings_are_shown_for_svn_repository(
345 327 self, autologin_user, backend_svn, csrf_token):
346 328 repo_name = backend_svn.repo_name
347 329 response = self.app.get(
348 330 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
349 331 response.mustcontain('Subversion Settings')
350 332
351 333 @pytest.mark.skip_backends('svn')
352 334 def test_svn_settings_are_not_created_for_not_svn_repository(
353 335 self, autologin_user, backend, csrf_token):
354 336 repo_name = backend.repo_name
355 337 data = self.FORM_DATA.copy()
356 338 data['csrf_token'] = csrf_token
357 339 self.app.post(
358 340 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
359 341 settings = SettingsModel(repo=repo_name)
360 342 try:
361 343 svn_branches = settings.get_ui_by_section(
362 344 VcsSettingsModel.SVN_BRANCH_SECTION)
363 345 svn_tags = settings.get_ui_by_section(
364 346 VcsSettingsModel.SVN_TAG_SECTION)
365 347 assert len(svn_branches) == 0
366 348 assert len(svn_tags) == 0
367 349 finally:
368 350 self._cleanup_repo_settings(settings)
369 351
370 352 @pytest.mark.skip_backends('svn')
371 353 def test_svn_settings_are_shown_only_for_svn_repository(
372 354 self, autologin_user, backend, csrf_token):
373 355 repo_name = backend.repo_name
374 356 response = self.app.get(
375 357 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
376 358 response.mustcontain(no='Subversion Settings')
377 359
378 360 def test_hg_settings_are_created(
379 361 self, autologin_user, backend_hg, csrf_token):
380 362 repo_name = backend_hg.repo_name
381 363 data = self.FORM_DATA.copy()
382 364 data['new_svn_tag'] = 'svn-tag'
383 365 data['new_svn_branch'] = 'svn-branch'
384 366 data['csrf_token'] = csrf_token
385 367 self.app.post(
386 368 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
387 369 settings = SettingsModel(repo=repo_name)
388 370 try:
389 371 largefiles_ui = settings.get_ui_by_section_and_key(
390 372 'extensions', 'largefiles')
391 373 assert largefiles_ui.ui_active is False
392 374 phases_ui = settings.get_ui_by_section_and_key(
393 375 'phases', 'publish')
394 376 assert str2bool(phases_ui.ui_value) is False
395 377 finally:
396 378 self._cleanup_repo_settings(settings)
397 379
398 380 def test_hg_settings_are_updated(
399 381 self, autologin_user, backend_hg, csrf_token):
400 382 repo_name = backend_hg.repo_name
401 383 settings = SettingsModel(repo=repo_name)
402 384 settings.create_ui_section_value(
403 385 'extensions', '', key='largefiles', active=True)
404 386 settings.create_ui_section_value(
405 387 'phases', '1', key='publish', active=True)
406 388
407 389 data = self.FORM_DATA.copy()
408 390 data['csrf_token'] = csrf_token
409 391 self.app.post(
410 392 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
411 393 try:
412 394 largefiles_ui = settings.get_ui_by_section_and_key(
413 395 'extensions', 'largefiles')
414 396 assert largefiles_ui.ui_active is False
415 397 phases_ui = settings.get_ui_by_section_and_key(
416 398 'phases', 'publish')
417 399 assert str2bool(phases_ui.ui_value) is False
418 400 finally:
419 401 self._cleanup_repo_settings(settings)
420 402
421 403 def test_hg_settings_are_shown_for_hg_repository(
422 404 self, autologin_user, backend_hg, csrf_token):
423 405 repo_name = backend_hg.repo_name
424 406 response = self.app.get(
425 407 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
426 408 response.mustcontain('Mercurial Settings')
427 409
428 410 @pytest.mark.skip_backends('hg')
429 411 def test_hg_settings_are_created_only_for_hg_repository(
430 412 self, autologin_user, backend, csrf_token):
431 413 repo_name = backend.repo_name
432 414 data = self.FORM_DATA.copy()
433 415 data['csrf_token'] = csrf_token
434 416 self.app.post(
435 417 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
436 418 settings = SettingsModel(repo=repo_name)
437 419 try:
438 420 largefiles_ui = settings.get_ui_by_section_and_key(
439 421 'extensions', 'largefiles')
440 422 assert largefiles_ui is None
441 423 phases_ui = settings.get_ui_by_section_and_key(
442 424 'phases', 'publish')
443 425 assert phases_ui is None
444 426 finally:
445 427 self._cleanup_repo_settings(settings)
446 428
447 429 @pytest.mark.skip_backends('hg')
448 430 def test_hg_settings_are_shown_only_for_hg_repository(
449 431 self, autologin_user, backend, csrf_token):
450 432 repo_name = backend.repo_name
451 433 response = self.app.get(
452 434 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
453 435 response.mustcontain(no='Mercurial Settings')
454 436
455 437 @pytest.mark.skip_backends('hg')
456 438 def test_hg_settings_are_updated_only_for_hg_repository(
457 439 self, autologin_user, backend, csrf_token):
458 440 repo_name = backend.repo_name
459 441 settings = SettingsModel(repo=repo_name)
460 442 settings.create_ui_section_value(
461 443 'extensions', '', key='largefiles', active=True)
462 444 settings.create_ui_section_value(
463 445 'phases', '1', key='publish', active=True)
464 446
465 447 data = self.FORM_DATA.copy()
466 448 data['csrf_token'] = csrf_token
467 449 self.app.post(
468 450 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
469 451 try:
470 452 largefiles_ui = settings.get_ui_by_section_and_key(
471 453 'extensions', 'largefiles')
472 454 assert largefiles_ui.ui_active is True
473 455 phases_ui = settings.get_ui_by_section_and_key(
474 456 'phases', 'publish')
475 457 assert phases_ui.ui_value == '1'
476 458 finally:
477 459 self._cleanup_repo_settings(settings)
478 460
479 461 def test_per_repo_svn_settings_are_displayed(
480 462 self, autologin_user, backend_svn, settings_util):
481 463 repo = backend_svn.create_repo()
482 464 repo_name = repo.repo_name
483 465 branches = [
484 466 settings_util.create_repo_rhodecode_ui(
485 467 repo, VcsSettingsModel.SVN_BRANCH_SECTION,
486 468 'branch_{}'.format(i))
487 469 for i in range(10)]
488 470 tags = [
489 471 settings_util.create_repo_rhodecode_ui(
490 472 repo, VcsSettingsModel.SVN_TAG_SECTION, 'tag_{}'.format(i))
491 473 for i in range(10)]
492 474
493 475 response = self.app.get(
494 476 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
495 477 assert_response = response.assert_response()
496 478 for branch in branches:
497 479 css_selector = '[name=branch_value_{}]'.format(branch.ui_id)
498 480 element = assert_response.get_element(css_selector)
499 481 assert element.value == branch.ui_value
500 482 for tag in tags:
501 483 css_selector = '[name=tag_ui_value_new_{}]'.format(tag.ui_id)
502 484 element = assert_response.get_element(css_selector)
503 485 assert element.value == tag.ui_value
504 486
505 487 def test_per_repo_hg_and_pr_settings_are_not_displayed_for_svn(
506 488 self, autologin_user, backend_svn, settings_util):
507 489 repo = backend_svn.create_repo()
508 490 repo_name = repo.repo_name
509 491 response = self.app.get(
510 492 route_path('edit_repo_vcs', repo_name=repo_name), status=200)
511 493 response.mustcontain(no='<label>Hooks:</label>')
512 494 response.mustcontain(no='<label>Pull Request Settings:</label>')
513 495
514 496 def test_inherit_global_settings_value_is_saved(
515 497 self, autologin_user, backend, csrf_token):
516 498 repo_name = backend.repo_name
517 499 data = self.FORM_DATA.copy()
518 500 data['csrf_token'] = csrf_token
519 501 data['inherit_global_settings'] = True
520 502 self.app.post(
521 503 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
522 504
523 505 settings = SettingsModel(repo=repo_name)
524 506 vcs_settings = VcsSettingsModel(repo=repo_name)
525 507 try:
526 508 assert vcs_settings.inherit_global_settings is True
527 509 finally:
528 510 self._cleanup_repo_settings(settings)
529 511
530 512 def test_repo_cache_is_invalidated_when_settings_are_updated(
531 513 self, autologin_user, backend, csrf_token):
532 514 repo_name = backend.repo_name
533 515 data = self.FORM_DATA.copy()
534 516 data['csrf_token'] = csrf_token
535 517 data['inherit_global_settings'] = True
536 518 settings = SettingsModel(repo=repo_name)
537 519
538 520 invalidation_patcher = mock.patch(
539 521 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
540 522 with invalidation_patcher as invalidation_mock:
541 523 self.app.post(
542 524 route_path('edit_repo_vcs_update', repo_name=repo_name), data,
543 525 status=302)
544 526 try:
545 527 invalidation_mock.assert_called_once_with(repo_name, delete=True)
546 528 finally:
547 529 self._cleanup_repo_settings(settings)
548 530
549 531 def test_other_settings_not_saved_inherit_global_settings_is_true(
550 532 self, autologin_user, backend, csrf_token):
551 533 repo_name = backend.repo_name
552 534 data = self.FORM_DATA.copy()
553 535 data['csrf_token'] = csrf_token
554 536 data['inherit_global_settings'] = True
555 537 self.app.post(
556 538 route_path('edit_repo_vcs_update', repo_name=repo_name), data, status=302)
557 539
558 540 settings = SettingsModel(repo=repo_name)
559 541 ui_settings = (
560 542 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
561 543
562 544 vcs_settings = []
563 545 try:
564 546 for section, key in ui_settings:
565 547 ui = settings.get_ui_by_section_and_key(section, key)
566 548 if ui:
567 549 vcs_settings.append(ui)
568 550 vcs_settings.extend(settings.get_ui_by_section(
569 551 VcsSettingsModel.SVN_BRANCH_SECTION))
570 552 vcs_settings.extend(settings.get_ui_by_section(
571 553 VcsSettingsModel.SVN_TAG_SECTION))
572 554 for name in VcsSettingsModel.GENERAL_SETTINGS:
573 555 setting = settings.get_setting_by_name(name)
574 556 if setting:
575 557 vcs_settings.append(setting)
576 558 assert vcs_settings == []
577 559 finally:
578 560 self._cleanup_repo_settings(settings)
579 561
580 562 def test_delete_svn_branch_and_tag_patterns(
581 563 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
582 564 repo = backend_svn.create_repo()
583 565 repo_name = repo.repo_name
584 566 branch = settings_util.create_repo_rhodecode_ui(
585 567 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
586 568 cleanup=False)
587 569 tag = settings_util.create_repo_rhodecode_ui(
588 570 repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag', cleanup=False)
589 571 data = {
590 572 'csrf_token': csrf_token
591 573 }
592 574 for id_ in (branch.ui_id, tag.ui_id):
593 575 data['delete_svn_pattern'] = id_,
594 576 self.app.post(
595 577 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
596 578 data, extra_environ=xhr_header, status=200)
597 579 settings = VcsSettingsModel(repo=repo_name)
598 580 assert settings.get_repo_svn_branch_patterns() == []
599 581
600 582 def test_delete_svn_branch_requires_repo_admin_permission(
601 583 self, backend_svn, user_util, settings_util, csrf_token, xhr_header):
602 584 repo = backend_svn.create_repo()
603 585 repo_name = repo.repo_name
604 586
605 587 logout_user_session(self.app, csrf_token)
606 588 session = login_user_session(
607 589 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
608 590 csrf_token = auth.get_csrf_token(session)
609 591
610 592 repo = Repository.get_by_repo_name(repo_name)
611 593 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
612 594 user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
613 595 branch = settings_util.create_repo_rhodecode_ui(
614 596 repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
615 597 cleanup=False)
616 598 data = {
617 599 'csrf_token': csrf_token,
618 600 'delete_svn_pattern': branch.ui_id
619 601 }
620 602 self.app.post(
621 603 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
622 604 data, extra_environ=xhr_header, status=200)
623 605
624 606 def test_delete_svn_branch_raises_400_when_not_found(
625 607 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
626 608 repo_name = backend_svn.repo_name
627 609 data = {
628 610 'delete_svn_pattern': 123,
629 611 'csrf_token': csrf_token
630 612 }
631 613 self.app.post(
632 614 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
633 615 data, extra_environ=xhr_header, status=400)
634 616
635 617 def test_delete_svn_branch_raises_400_when_no_id_specified(
636 618 self, autologin_user, backend_svn, settings_util, csrf_token, xhr_header):
637 619 repo_name = backend_svn.repo_name
638 620 data = {
639 621 'csrf_token': csrf_token
640 622 }
641 623 self.app.post(
642 624 route_path('edit_repo_vcs_svn_pattern_delete', repo_name=repo_name),
643 625 data, extra_environ=xhr_header, status=400)
644 626
645 627 def _cleanup_repo_settings(self, settings_model):
646 628 cleanup = []
647 629 ui_settings = (
648 630 VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
649 631
650 632 for section, key in ui_settings:
651 633 ui = settings_model.get_ui_by_section_and_key(section, key)
652 634 if ui:
653 635 cleanup.append(ui)
654 636
655 637 cleanup.extend(settings_model.get_ui_by_section(
656 638 VcsSettingsModel.INHERIT_SETTINGS))
657 639 cleanup.extend(settings_model.get_ui_by_section(
658 640 VcsSettingsModel.SVN_BRANCH_SECTION))
659 641 cleanup.extend(settings_model.get_ui_by_section(
660 642 VcsSettingsModel.SVN_TAG_SECTION))
661 643
662 644 for name in VcsSettingsModel.GENERAL_SETTINGS:
663 645 setting = settings_model.get_setting_by_name(name)
664 646 if setting:
665 647 cleanup.append(setting)
666 648
667 649 for object_ in cleanup:
668 650 Session().delete(object_)
669 651 Session().commit()
670 652
671 653 def assert_repo_value_equals_global_value(self, response, setting):
672 654 assert_response = response.assert_response()
673 655 global_css_selector = '[name={}_inherited]'.format(setting)
674 656 repo_css_selector = '[name={}]'.format(setting)
675 657 repo_element = assert_response.get_element(repo_css_selector)
676 658 global_element = assert_response.get_element(global_css_selector)
677 659 assert repo_element.value == global_element.value
678 660
679 661
680 662 def _get_permission_for_user(user, repo):
681 663 perm = UserRepoToPerm.query()\
682 664 .filter(UserRepoToPerm.repository ==
683 665 Repository.get_by_repo_name(repo))\
684 666 .filter(UserRepoToPerm.user == User.get_by_username(user))\
685 667 .all()
686 668 return perm
@@ -1,105 +1,89 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.model.db import Repository
25 25 from rhodecode.model.settings import SettingsModel
26 from rhodecode.tests.utils import AssertResponse
27
28
29 def route_path(name, params=None, **kwargs):
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'edit_repo': '/{repo_name}/settings',
36 'edit_repo_vcs': '/{repo_name}/settings/vcs',
37 'edit_repo_vcs_update': '/{repo_name}/settings/vcs/update',
38 }[name].format(**kwargs)
39
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
26 from rhodecode.tests.routes import route_path
43 27
44 28
45 29 @pytest.mark.usefixtures('autologin_user', 'app')
46 30 class TestAdminRepoVcsSettings(object):
47 31
48 32 @pytest.mark.parametrize('setting_name, setting_backends', [
49 33 ('hg_use_rebase_for_merging', ['hg']),
50 34 ])
51 35 def test_labs_settings_visible_if_enabled(
52 36 self, setting_name, setting_backends, backend):
53 37 if backend.alias not in setting_backends:
54 38 pytest.skip('Setting not available for backend {}'.format(backend))
55 39
56 40 vcs_settings_url = route_path(
57 41 'edit_repo_vcs', repo_name=backend.repo.repo_name)
58 42
59 43 with mock.patch.dict(
60 44 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
61 45 response = self.app.get(vcs_settings_url)
62 46
63 47 assertr = response.assert_response()
64 48 assertr.one_element_exists('#rhodecode_{}'.format(setting_name))
65 49
66 50 @pytest.mark.parametrize('setting_name, setting_backends', [
67 51 ('hg_use_rebase_for_merging', ['hg']),
68 52 ])
69 53 def test_update_boolean_settings(
70 54 self, csrf_token, setting_name, setting_backends, backend):
71 55 if backend.alias not in setting_backends:
72 56 pytest.skip('Setting not available for backend {}'.format(backend))
73 57
74 58 repo = backend.create_repo()
75 59 repo_name = repo.repo_name
76 60
77 61 settings_model = SettingsModel(repo=repo)
78 62 vcs_settings_url = route_path(
79 63 'edit_repo_vcs_update', repo_name=repo_name)
80 64
81 65 self.app.post(
82 66 vcs_settings_url,
83 67 params={
84 68 'inherit_global_settings': False,
85 69 'new_svn_branch': 'dummy-value-for-testing',
86 70 'new_svn_tag': 'dummy-value-for-testing',
87 71 'rhodecode_{}'.format(setting_name): 'true',
88 72 'csrf_token': csrf_token,
89 73 })
90 74 settings_model = SettingsModel(repo=Repository.get_by_repo_name(repo_name))
91 75 setting = settings_model.get_setting_by_name(setting_name)
92 76 assert setting.app_settings_value
93 77
94 78 self.app.post(
95 79 vcs_settings_url,
96 80 params={
97 81 'inherit_global_settings': False,
98 82 'new_svn_branch': 'dummy-value-for-testing',
99 83 'new_svn_tag': 'dummy-value-for-testing',
100 84 'rhodecode_{}'.format(setting_name): 'false',
101 85 'csrf_token': csrf_token,
102 86 })
103 87 settings_model = SettingsModel(repo=Repository.get_by_repo_name(repo_name))
104 88 setting = settings_model.get_setting_by_name(setting_name)
105 89 assert not setting.app_settings_value
@@ -1,208 +1,197 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20
21 21 import mock
22 22 import pytest
23 23 from whoosh import query
24 24
25 25 from rhodecode.tests import (
26 TestController, route_path_generator, HG_REPO,
26 TestController, HG_REPO,
27 27 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
28 from rhodecode.tests.utils import AssertResponse
29
30
31 def route_path(name, params=None, **kwargs):
32 from rhodecode.apps._base import ADMIN_PREFIX
33 url_defs = {
34 'search':
35 ADMIN_PREFIX + '/search',
36 'search_repo':
37 '/{repo_name}/search',
38 }
39 return route_path_generator(url_defs, name=name, params=params, **kwargs)
28 from rhodecode.tests.routes import route_path
40 29
41 30
42 31 class TestSearchController(TestController):
43 32
44 33 def test_index(self):
45 34 self.log_user()
46 35 response = self.app.get(route_path('search'))
47 36 assert_response = response.assert_response()
48 37 assert_response.one_element_exists('input#q')
49 38
50 39 def test_search_files_empty_search(self):
51 40 if os.path.isdir(self.index_location):
52 41 pytest.skip('skipped due to existing index')
53 42 else:
54 43 self.log_user()
55 44 response = self.app.get(route_path('search'),
56 45 {'q': HG_REPO})
57 46 response.mustcontain('There is no index to search in. '
58 47 'Please run whoosh indexer')
59 48
60 49 def test_search_validation(self):
61 50 self.log_user()
62 51 response = self.app.get(route_path('search'),
63 52 {'q': query, 'type': 'content', 'page_limit': 1000})
64 53
65 54 response.mustcontain(
66 55 'page_limit - 1000 is greater than maximum value 500')
67 56
68 57 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
69 58 ('todo', 23, [
70 59 'vcs/backends/hg/inmemory.py',
71 60 'vcs/tests/test_git.py']),
72 61 ('extension:rst installation', 6, [
73 62 'docs/index.rst',
74 63 'docs/installation.rst']),
75 64 ('def repo', 87, [
76 65 'vcs/tests/test_git.py',
77 66 'vcs/tests/test_changesets.py']),
78 67 ('repository:%s def test' % HG_REPO, 18, [
79 68 'vcs/tests/test_git.py',
80 69 'vcs/tests/test_changesets.py']),
81 70 ('"def main"', 9, [
82 71 'vcs/__init__.py',
83 72 'vcs/tests/__init__.py',
84 73 'vcs/utils/progressbar.py']),
85 74 ('owner:test_admin', 358, [
86 75 'vcs/tests/base.py',
87 76 'MANIFEST.in',
88 77 'vcs/utils/termcolors.py',
89 78 'docs/theme/ADC/static/documentation.png']),
90 79 ('owner:test_admin def main', 72, [
91 80 'vcs/__init__.py',
92 81 'vcs/tests/test_utils_filesize.py',
93 82 'vcs/tests/test_cli.py']),
94 83 ('owner:michał test', 0, []),
95 84 ])
96 85 def test_search_files(self, query, expected_hits, expected_paths):
97 86 self.log_user()
98 87 response = self.app.get(route_path('search'),
99 88 {'q': query, 'type': 'content', 'page_limit': 500})
100 89
101 90 response.mustcontain('%s results' % expected_hits)
102 91 for path in expected_paths:
103 92 response.mustcontain(path)
104 93
105 94 @pytest.mark.parametrize("query, expected_hits, expected_commits", [
106 95 ('bother to ask where to fetch repo during tests', 3, [
107 96 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'),
108 97 ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'),
109 98 ('svn', '98')]),
110 99 ('michał', 0, []),
111 100 ('changed:tests/utils.py', 36, [
112 101 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]),
113 102 ('changed:vcs/utils/archivers.py', 11, [
114 103 ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'),
115 104 ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'),
116 105 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
117 106 ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'),
118 107 ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'),
119 108 ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'),
120 109 ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'),
121 110 ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]),
122 111 ('added:README.rst', 3, [
123 112 ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'),
124 113 ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
125 114 ('svn', '8')]),
126 115 ('changed:lazy.py', 15, [
127 116 ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'),
128 117 ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'),
129 118 ('svn', '82'),
130 119 ('svn', '262'),
131 120 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
132 121 ('git', '33fa3223355104431402a888fa77a4e9956feb3e')
133 122 ]),
134 123 ('author:marcin@python-blog.com '
135 124 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
136 125 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
137 126 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
138 127 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
139 128 ('b986218b', 1, [
140 129 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
141 130 ])
142 131 def test_search_commit_messages(
143 132 self, query, expected_hits, expected_commits, enabled_backends):
144 133 self.log_user()
145 134 response = self.app.get(route_path('search'),
146 135 {'q': query, 'type': 'commit', 'page_limit': 500})
147 136
148 137 response.mustcontain('%s results' % expected_hits)
149 138 for backend, commit_id in expected_commits:
150 139 if backend in enabled_backends:
151 140 response.mustcontain(commit_id)
152 141
153 142 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
154 143 ('readme.rst', 3, []),
155 144 ('test*', 75, []),
156 145 ('*model*', 1, []),
157 146 ('extension:rst', 48, []),
158 147 ('extension:rst api', 24, []),
159 148 ])
160 149 def test_search_file_paths(self, query, expected_hits, expected_paths):
161 150 self.log_user()
162 151 response = self.app.get(route_path('search'),
163 152 {'q': query, 'type': 'path', 'page_limit': 500})
164 153
165 154 response.mustcontain('%s results' % expected_hits)
166 155 for path in expected_paths:
167 156 response.mustcontain(path)
168 157
169 158 def test_search_commit_message_specific_repo(self, backend):
170 159 self.log_user()
171 160 response = self.app.get(
172 161 route_path('search_repo',repo_name=backend.repo_name),
173 162 {'q': 'bother to ask where to fetch repo during tests',
174 163 'type': 'commit'})
175 164
176 165 response.mustcontain('1 results')
177 166
178 167 def test_filters_are_not_applied_for_admin_user(self):
179 168 self.log_user()
180 169 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
181 170 search_mock.return_value = mock.MagicMock(
182 171 scored_length=lambda: 100,
183 172 runtime=10
184 173 )
185 174 self.app.get(route_path('search'),
186 175 {'q': 'test query', 'type': 'commit'})
187 176 assert search_mock.call_count == 1
188 177 _, kwargs = search_mock.call_args
189 178 assert kwargs['filter'] is None
190 179
191 180 def test_filters_are_applied_for_normal_user(self, enabled_backends):
192 181 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
193 182 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
194 183 search_mock.return_value = mock.MagicMock(
195 184 scored_length=lambda: 100,
196 185 runtime=10
197 186 )
198 187 self.app.get(route_path('search'),
199 188 {'q': 'test query', 'type': 'commit'})
200 189 assert search_mock.call_count == 1
201 190 _, kwargs = search_mock.call_args
202 191 assert isinstance(kwargs['filter'], query.Or)
203 192 expected_repositories = [
204 193 f'vcs_test_{b}' for b in enabled_backends]
205 194 queried_repositories = [
206 195 name for type_, name in kwargs['filter'].all_terms()]
207 196 for repository in expected_repositories:
208 197 assert repository in queried_repositories
@@ -1,254 +1,230 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests import (
22 22 TestController, assert_session_flash, TEST_USER_ADMIN_LOGIN)
23 23 from rhodecode.model.db import UserGroup
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.routes import route_path
26 27
27 28 fixture = Fixture()
28 29
29 30
30 def route_path(name, params=None, **kwargs):
31 import urllib.request
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'user_groups': ADMIN_PREFIX + '/user_groups',
38 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
39 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
40 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
41 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
42 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
43 'edit_user_group_advanced_sync': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/advanced/sync',
44 'edit_user_group_global_perms_update': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/global_permissions/update',
45 'user_groups_update': ADMIN_PREFIX + '/user_groups/{user_group_id}/update',
46 'user_groups_delete': ADMIN_PREFIX + '/user_groups/{user_group_id}/delete',
47
48 }[name].format(**kwargs)
49
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 return base_url
53
54
55 31 class TestUserGroupsView(TestController):
56 32
57 33 def test_set_synchronization(self, user_util):
58 34 self.log_user()
59 35 user_group_name = user_util.create_user_group().users_group_name
60 36
61 37 group = Session().query(UserGroup).filter(
62 38 UserGroup.users_group_name == user_group_name).one()
63 39
64 40 assert group.group_data.get('extern_type') is None
65 41
66 42 # enable
67 43 self.app.post(
68 44 route_path('edit_user_group_advanced_sync',
69 45 user_group_id=group.users_group_id),
70 46 params={'csrf_token': self.csrf_token}, status=302)
71 47
72 48 group = Session().query(UserGroup).filter(
73 49 UserGroup.users_group_name == user_group_name).one()
74 50 assert group.group_data.get('extern_type') == 'manual'
75 51 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
76 52
77 53 # disable
78 54 self.app.post(
79 55 route_path('edit_user_group_advanced_sync',
80 56 user_group_id=group.users_group_id),
81 57 params={'csrf_token': self.csrf_token}, status=302)
82 58
83 59 group = Session().query(UserGroup).filter(
84 60 UserGroup.users_group_name == user_group_name).one()
85 61 assert group.group_data.get('extern_type') is None
86 62 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
87 63
88 64 def test_delete_user_group(self, user_util):
89 65 self.log_user()
90 66 user_group_id = user_util.create_user_group().users_group_id
91 67
92 68 group = Session().query(UserGroup).filter(
93 69 UserGroup.users_group_id == user_group_id).one()
94 70
95 71 self.app.post(
96 72 route_path('user_groups_delete', user_group_id=group.users_group_id),
97 73 params={'csrf_token': self.csrf_token})
98 74
99 75 group = Session().query(UserGroup).filter(
100 76 UserGroup.users_group_id == user_group_id).scalar()
101 77
102 78 assert group is None
103 79
104 80 @pytest.mark.parametrize('repo_create, repo_create_write, user_group_create, repo_group_create, fork_create, inherit_default_permissions, expect_error, expect_form_error', [
105 81 ('hg.create.none', 'hg.create.write_on_repogroup.false', 'hg.usergroup.create.false', 'hg.repogroup.create.false', 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
106 82 ('hg.create.repository', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, False),
107 83 ('hg.create.XXX', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, True),
108 84 ('', '', '', '', '', '', True, False),
109 85 ])
110 86 def test_global_permissions_on_user_group(
111 87 self, repo_create, repo_create_write, user_group_create,
112 88 repo_group_create, fork_create, expect_error, expect_form_error,
113 89 inherit_default_permissions, user_util):
114 90
115 91 self.log_user()
116 92 user_group = user_util.create_user_group()
117 93
118 94 user_group_name = user_group.users_group_name
119 95 user_group_id = user_group.users_group_id
120 96
121 97 # ENABLE REPO CREATE ON A GROUP
122 98 perm_params = {
123 99 'inherit_default_permissions': False,
124 100 'default_repo_create': repo_create,
125 101 'default_repo_create_on_write': repo_create_write,
126 102 'default_user_group_create': user_group_create,
127 103 'default_repo_group_create': repo_group_create,
128 104 'default_fork_create': fork_create,
129 105 'default_inherit_default_permissions': inherit_default_permissions,
130 106
131 107 'csrf_token': self.csrf_token,
132 108 }
133 109 response = self.app.post(
134 110 route_path('edit_user_group_global_perms_update',
135 111 user_group_id=user_group_id),
136 112 params=perm_params)
137 113
138 114 if expect_form_error:
139 115 assert response.status_int == 200
140 116 response.mustcontain('Value must be one of')
141 117 else:
142 118 if expect_error:
143 119 msg = 'An error occurred during permissions saving'
144 120 else:
145 121 msg = 'User Group global permissions updated successfully'
146 122 ug = UserGroup.get_by_group_name(user_group_name)
147 123 del perm_params['csrf_token']
148 124 del perm_params['inherit_default_permissions']
149 125 assert perm_params == ug.get_default_perms()
150 126 assert_session_flash(response, msg)
151 127
152 128 def test_edit_view(self, user_util):
153 129 self.log_user()
154 130
155 131 user_group = user_util.create_user_group()
156 132 self.app.get(
157 133 route_path('edit_user_group',
158 134 user_group_id=user_group.users_group_id),
159 135 status=200)
160 136
161 137 def test_update_user_group(self, user_util):
162 138 user = self.log_user()
163 139
164 140 user_group = user_util.create_user_group()
165 141 users_group_id = user_group.users_group_id
166 142 new_name = user_group.users_group_name + '_CHANGE'
167 143
168 144 params = [
169 145 ('users_group_active', False),
170 146 ('user_group_description', 'DESC'),
171 147 ('users_group_name', new_name),
172 148 ('user', user['username']),
173 149 ('csrf_token', self.csrf_token),
174 150 ('__start__', 'user_group_members:sequence'),
175 151 ('__start__', 'member:mapping'),
176 152 ('member_user_id', user['user_id']),
177 153 ('type', 'existing'),
178 154 ('__end__', 'member:mapping'),
179 155 ('__end__', 'user_group_members:sequence'),
180 156 ]
181 157
182 158 self.app.post(
183 159 route_path('user_groups_update',
184 160 user_group_id=users_group_id),
185 161 params=params,
186 162 status=302)
187 163
188 164 user_group = UserGroup.get(users_group_id)
189 165 assert user_group
190 166
191 167 assert user_group.users_group_name == new_name
192 168 assert user_group.user_group_description == 'DESC'
193 169 assert user_group.users_group_active is False
194 170
195 171 def test_update_user_group_name_conflicts(self, user_util):
196 172 self.log_user()
197 173 user_group_old = user_util.create_user_group()
198 174 new_name = user_group_old.users_group_name
199 175
200 176 user_group = user_util.create_user_group()
201 177
202 178 params = dict(
203 179 users_group_active=False,
204 180 user_group_description='DESC',
205 181 users_group_name=new_name,
206 182 csrf_token=self.csrf_token)
207 183
208 184 response = self.app.post(
209 185 route_path('user_groups_update',
210 186 user_group_id=user_group.users_group_id),
211 187 params=params,
212 188 status=200)
213 189
214 190 response.mustcontain('User group `{}` already exists'.format(
215 191 new_name))
216 192
217 193 def test_update_members_from_user_ids(self, user_regular):
218 194 uid = user_regular.user_id
219 195 username = user_regular.username
220 196 self.log_user()
221 197
222 198 user_group = fixture.create_user_group('test_gr_ids')
223 199 assert user_group.members == []
224 200 assert user_group.user != user_regular
225 201 expected_active_state = not user_group.users_group_active
226 202
227 203 form_data = [
228 204 ('csrf_token', self.csrf_token),
229 205 ('user', username),
230 206 ('users_group_name', 'changed_name'),
231 207 ('users_group_active', expected_active_state),
232 208 ('user_group_description', 'changed_description'),
233 209
234 210 ('__start__', 'user_group_members:sequence'),
235 211 ('__start__', 'member:mapping'),
236 212 ('member_user_id', uid),
237 213 ('type', 'existing'),
238 214 ('__end__', 'member:mapping'),
239 215 ('__end__', 'user_group_members:sequence'),
240 216 ]
241 217 ugid = user_group.users_group_id
242 218 self.app.post(
243 219 route_path('user_groups_update', user_group_id=ugid), form_data)
244 220
245 221 user_group = UserGroup.get(ugid)
246 222 assert user_group
247 223
248 224 assert user_group.members[0].user_id == uid
249 225 assert user_group.user_id == uid
250 226 assert 'changed_name' in user_group.users_group_name
251 227 assert 'changed_description' in user_group.user_group_description
252 228 assert user_group.users_group_active == expected_active_state
253 229
254 230 fixture.destroy_user_group(user_group)
@@ -1,80 +1,63 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests.utils import permission_update_data_generator
22
23
24 def route_path(name, params=None, **kwargs):
25 import urllib.request
26 import urllib.parse
27 import urllib.error
28 from rhodecode.apps._base import ADMIN_PREFIX
29
30 base_url = {
31 'edit_user_group_perms':
32 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions',
33 'edit_user_group_perms_update':
34 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions/update',
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
22 from rhodecode.tests.routes import route_path
40 23
41 24
42 25 @pytest.mark.usefixtures("app")
43 26 class TestUserGroupPermissionsView(object):
44 27
45 28 def test_edit_perms_view(self, user_util, autologin_user):
46 29 user_group = user_util.create_user_group()
47 30 self.app.get(
48 31 route_path('edit_user_group_perms',
49 32 user_group_id=user_group.users_group_id), status=200)
50 33
51 34 def test_update_permissions(self, csrf_token, user_util):
52 35 user_group = user_util.create_user_group()
53 36 user_group_id = user_group.users_group_id
54 37 user = user_util.create_user()
55 38 user_id = user.user_id
56 39 username = user.username
57 40
58 41 # grant new
59 42 form_data = permission_update_data_generator(
60 43 csrf_token,
61 44 default='usergroup.write',
62 45 grant=[(user_id, 'usergroup.write', username, 'user')])
63 46
64 47 response = self.app.post(
65 48 route_path('edit_user_group_perms_update',
66 49 user_group_id=user_group_id), form_data).follow()
67 50
68 51 assert 'User Group permissions updated' in response
69 52
70 53 # revoke given
71 54 form_data = permission_update_data_generator(
72 55 csrf_token,
73 56 default='usergroup.read',
74 57 revoke=[(user_id, 'user')])
75 58
76 59 response = self.app.post(
77 60 route_path('edit_user_group_perms_update',
78 61 user_group_id=user_group_id), form_data).follow()
79 62
80 63 assert 'User Group permissions updated' in response
@@ -1,74 +1,70 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 from rhodecode.model.user_group import UserGroupModel
19 19 from rhodecode.tests import (
20 20 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
21 21 from rhodecode.tests.fixture import Fixture
22 from rhodecode.tests.utils import AssertResponse
22 from rhodecode.tests.routes import route_path
23 23
24 24 fixture = Fixture()
25 25
26 26
27 def route_path(name, **kwargs):
28 return '/_profile_user_group/{user_group_name}'.format(**kwargs)
29
30
31 27 class TestUsersController(TestController):
32 28
33 29 def test_user_group_profile(self, user_util):
34 30 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
35 31 user, usergroup = user_util.create_user_with_group()
36 32
37 33 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
38 34 response.mustcontain(usergroup.users_group_name)
39 35 response.mustcontain(user.username)
40 36
41 37 def test_user_can_check_own_group(self, user_util):
42 38 user = user_util.create_user(
43 39 TEST_USER_REGULAR_LOGIN, password=TEST_USER_REGULAR_PASS, email='testme@rhodecode.org')
44 40 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
45 41 usergroup = user_util.create_user_group(owner=user)
46 42 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
47 43 response.mustcontain(usergroup.users_group_name)
48 44 response.mustcontain(user.username)
49 45
50 46 def test_user_can_not_check_other_group(self, user_util):
51 47 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
52 48 user_group = user_util.create_user_group()
53 49 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.none')
54 50 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=404)
55 51 assert response.status_code == 404
56 52
57 53 def test_another_user_can_check_if_he_is_in_group(self, user_util):
58 54 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
59 55 user = user_util.create_user(
60 56 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
61 57 user_group = user_util.create_user_group()
62 58 UserGroupModel().add_user_to_group(user_group, user)
63 59 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.read')
64 60 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name))
65 61 response.mustcontain(user_group.users_group_name)
66 62 response.mustcontain(user.username)
67 63
68 64 def test_with_anonymous_user(self, user_util):
69 65 user = user_util.create_user(
70 66 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
71 67 user_group = user_util.create_user_group()
72 68 UserGroupModel().add_user_to_group(user_group, user)
73 69 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=302)
74 70 assert response.status_code == 302 No newline at end of file
@@ -1,73 +1,70 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.model.db import User
22 22 from rhodecode.tests import (
23 23 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
24 24 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.tests.utils import AssertResponse
27 from rhodecode.tests.routes import route_path
27 28
28 29 fixture = Fixture()
29 30
30 31
31 def route_path(name, **kwargs):
32 return '/_profiles/{username}'.format(**kwargs)
33
34
35 32 class TestUsersController(TestController):
36 33
37 34 def test_user_profile(self, user_util):
38 35 edit_link_css = '.user-profile .panel-edit'
39 36 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
40 37 user = user_util.create_user(
41 38 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
42 39 username = user.username
43 40
44 41 response = self.app.get(route_path('user_profile', username=username))
45 42 response.mustcontain('testme')
46 43 response.mustcontain('testme@rhodecode.org')
47 44 assert_response = response.assert_response()
48 45 assert_response.no_element_exists(edit_link_css)
49 46
50 47 # edit should be available to superadmin users
51 48 self.logout_user()
52 49 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
53 50 response = self.app.get(route_path('user_profile', username=username))
54 51 assert_response = response.assert_response()
55 52 assert_response.element_contains(edit_link_css, 'Edit')
56 53
57 54 def test_user_profile_not_available(self, user_util):
58 55 user = user_util.create_user()
59 56 username = user.username
60 57
61 58 # not logged in, redirect
62 59 self.app.get(route_path('user_profile', username=username), status=302)
63 60
64 61 self.log_user()
65 62 # after log-in show
66 63 self.app.get(route_path('user_profile', username=username), status=200)
67 64
68 65 # default user, not allowed to show it
69 66 self.app.get(
70 67 route_path('user_profile', username=User.DEFAULT_USER), status=404)
71 68
72 69 # actual 404
73 70 self.app.get(route_path('user_profile', username='unknown'), status=404)
@@ -1,630 +1,632 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import sys
21 21 import collections
22 22 import tempfile
23 23 import time
24 24 import logging.config
25 25
26 26 from paste.gzipper import make_gzip_middleware
27 27 import pyramid.events
28 28 from pyramid.wsgi import wsgiapp
29 29 from pyramid.authorization import ACLAuthorizationPolicy
30 30 from pyramid.config import Configurator
31 31 from pyramid.settings import asbool, aslist
32 32 from pyramid.httpexceptions import (
33 33 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 34 from pyramid.renderers import render_to_response
35 35
36 36 from rhodecode.model import meta
37 37 from rhodecode.config import patches
38 38 from rhodecode.config import utils as config_utils
39 39 from rhodecode.config.settings_maker import SettingsMaker
40 40 from rhodecode.config.environment import load_pyramid_environment
41 41
42 42 import rhodecode.events
43 43 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 44 from rhodecode.lib.request import Request
45 45 from rhodecode.lib.vcs import VCSCommunicationError
46 46 from rhodecode.lib.exceptions import VCSServerUnavailable
47 47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 50 from rhodecode.lib.utils2 import AttributeDict
51 51 from rhodecode.lib.exc_tracking import store_exception, format_exc
52 52 from rhodecode.subscribers import (
53 53 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 54 write_metadata_if_needed, write_usage_data)
55 55 from rhodecode.lib.statsd_client import StatsdClient
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 def is_http_error(response):
61 61 # error which should have traceback
62 62 return response.status_code > 499
63 63
64 64
65 65 def should_load_all():
66 66 """
67 67 Returns if all application components should be loaded. In some cases it's
68 68 desired to skip apps loading for faster shell script execution
69 69 """
70 70 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
71 71 if ssh_cmd:
72 72 return False
73 73
74 74 return True
75 75
76 76
77 77 def make_pyramid_app(global_config, **settings):
78 78 """
79 79 Constructs the WSGI application based on Pyramid.
80 80
81 81 Specials:
82 82
83 83 * The application can also be integrated like a plugin via the call to
84 84 `includeme`. This is accompanied with the other utility functions which
85 85 are called. Changing this should be done with great care to not break
86 86 cases when these fragments are assembled from another place.
87 87
88 88 """
89 89 start_time = time.time()
90 90 log.info('Pyramid app config starting')
91 91
92 92 sanitize_settings_and_apply_defaults(global_config, settings)
93 93
94 94 # init and bootstrap StatsdClient
95 95 StatsdClient.setup(settings)
96 96
97 97 config = Configurator(settings=settings)
98 98 # Init our statsd at very start
99 99 config.registry.statsd = StatsdClient.statsd
100 100
101 101 # Apply compatibility patches
102 102 patches.inspect_getargspec()
103 103
104 104 load_pyramid_environment(global_config, settings)
105 105
106 106 # Static file view comes first
107 107 includeme_first(config)
108 108
109 109 includeme(config)
110 110
111 111 pyramid_app = config.make_wsgi_app()
112 112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
113 113 pyramid_app.config = config
114 114
115 115 celery_settings = get_celery_config(settings)
116 116 config.configure_celery(celery_settings)
117 117
118 118 # creating the app uses a connection - return it after we are done
119 119 meta.Session.remove()
120 120
121 121 total_time = time.time() - start_time
122 122 log.info('Pyramid app created and configured in %.2fs', total_time)
123 123 return pyramid_app
124 124
125 125
126 126 def get_celery_config(settings):
127 127 """
128 128 Converts basic ini configuration into celery 4.X options
129 129 """
130 130
131 131 def key_converter(key_name):
132 132 pref = 'celery.'
133 133 if key_name.startswith(pref):
134 134 return key_name[len(pref):].replace('.', '_').lower()
135 135
136 136 def type_converter(parsed_key, value):
137 137 # cast to int
138 138 if value.isdigit():
139 139 return int(value)
140 140
141 141 # cast to bool
142 142 if value.lower() in ['true', 'false', 'True', 'False']:
143 143 return value.lower() == 'true'
144 144 return value
145 145
146 146 celery_config = {}
147 147 for k, v in settings.items():
148 148 pref = 'celery.'
149 149 if k.startswith(pref):
150 150 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
151 151
152 152 # TODO:rethink if we want to support celerybeat based file config, probably NOT
153 153 # beat_config = {}
154 154 # for section in parser.sections():
155 155 # if section.startswith('celerybeat:'):
156 156 # name = section.split(':', 1)[1]
157 157 # beat_config[name] = get_beat_config(parser, section)
158 158
159 159 # final compose of settings
160 160 celery_settings = {}
161 161
162 162 if celery_config:
163 163 celery_settings.update(celery_config)
164 164 # if beat_config:
165 165 # celery_settings.update({'beat_schedule': beat_config})
166 166
167 167 return celery_settings
168 168
169 169
170 170 def not_found_view(request):
171 171 """
172 172 This creates the view which should be registered as not-found-view to
173 173 pyramid.
174 174 """
175 175
176 176 if not getattr(request, 'vcs_call', None):
177 177 # handle like regular case with our error_handler
178 178 return error_handler(HTTPNotFound(), request)
179 179
180 180 # handle not found view as a vcs call
181 181 settings = request.registry.settings
182 182 ae_client = getattr(request, 'ae_client', None)
183 183 vcs_app = VCSMiddleware(
184 184 HTTPNotFound(), request.registry, settings,
185 185 appenlight_client=ae_client)
186 186
187 187 return wsgiapp(vcs_app)(None, request)
188 188
189 189
190 190 def error_handler(exception, request):
191 191 import rhodecode
192 192 from rhodecode.lib import helpers
193 193
194 194 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
195 195
196 196 base_response = HTTPInternalServerError()
197 197 # prefer original exception for the response since it may have headers set
198 198 if isinstance(exception, HTTPException):
199 199 base_response = exception
200 200 elif isinstance(exception, VCSCommunicationError):
201 201 base_response = VCSServerUnavailable()
202 202
203 203 if is_http_error(base_response):
204 204 traceback_info = format_exc(request.exc_info)
205 205 log.error(
206 206 'error occurred handling this request for path: %s, \n%s',
207 207 request.path, traceback_info)
208 208
209 209 error_explanation = base_response.explanation or str(base_response)
210 210 if base_response.status_code == 404:
211 211 error_explanation += " Optionally you don't have permission to access this page."
212 212 c = AttributeDict()
213 213 c.error_message = base_response.status
214 214 c.error_explanation = error_explanation
215 215 c.visual = AttributeDict()
216 216
217 217 c.visual.rhodecode_support_url = (
218 218 request.registry.settings.get('rhodecode_support_url') or
219 219 request.route_url('rhodecode_support')
220 220 )
221 221 c.redirect_time = 0
222 222 c.rhodecode_name = rhodecode_title
223 223 if not c.rhodecode_name:
224 224 c.rhodecode_name = 'Rhodecode'
225 225
226 226 c.causes = []
227 227 if is_http_error(base_response):
228 228 c.causes.append('Server is overloaded.')
229 229 c.causes.append('Server database connection is lost.')
230 230 c.causes.append('Server expected unhandled error.')
231 231
232 232 if hasattr(base_response, 'causes'):
233 233 c.causes = base_response.causes
234 234
235 235 c.messages = helpers.flash.pop_messages(request=request)
236 236 exc_info = sys.exc_info()
237 237 c.exception_id = id(exc_info)
238 238 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
239 239 or base_response.status_code > 499
240 240 c.exception_id_url = request.route_url(
241 241 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
242 242
243 243 debug_mode = rhodecode.ConfigGet().get_bool('debug')
244 244 if c.show_exception_id:
245 245 store_exception(c.exception_id, exc_info)
246 246 c.exception_debug = debug_mode
247 247 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
248 248
249 249 if debug_mode:
250 250 try:
251 251 from rich.traceback import install
252 252 install(show_locals=True)
253 253 log.debug('Installing rich tracebacks...')
254 254 except ImportError:
255 255 pass
256 256
257 257 response = render_to_response(
258 258 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
259 259 response=base_response)
260 260
261 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
262
261 263 statsd = request.registry.statsd
262 264 if statsd and base_response.status_code > 499:
263 265 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
264 266 statsd.incr('rhodecode_exception_total',
265 267 tags=["exc_source:web",
266 268 f"http_code:{base_response.status_code}",
267 269 f"type:{exc_type}"])
268 270
269 271 return response
270 272
271 273
272 274 def includeme_first(config):
273 275 # redirect automatic browser favicon.ico requests to correct place
274 276 def favicon_redirect(context, request):
275 277 return HTTPFound(
276 278 request.static_path('rhodecode:public/images/favicon.ico'))
277 279
278 280 config.add_view(favicon_redirect, route_name='favicon')
279 281 config.add_route('favicon', '/favicon.ico')
280 282
281 283 def robots_redirect(context, request):
282 284 return HTTPFound(
283 285 request.static_path('rhodecode:public/robots.txt'))
284 286
285 287 config.add_view(robots_redirect, route_name='robots')
286 288 config.add_route('robots', '/robots.txt')
287 289
288 290 config.add_static_view(
289 291 '_static/deform', 'deform:static')
290 292 config.add_static_view(
291 293 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
292 294
293 295
294 296 ce_auth_resources = [
295 297 'rhodecode.authentication.plugins.auth_crowd',
296 298 'rhodecode.authentication.plugins.auth_headers',
297 299 'rhodecode.authentication.plugins.auth_jasig_cas',
298 300 'rhodecode.authentication.plugins.auth_ldap',
299 301 'rhodecode.authentication.plugins.auth_pam',
300 302 'rhodecode.authentication.plugins.auth_rhodecode',
301 303 'rhodecode.authentication.plugins.auth_token',
302 304 ]
303 305
304 306
305 307 def includeme(config, auth_resources=None):
306 308 from rhodecode.lib.celerylib.loader import configure_celery
307 309 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
308 310 settings = config.registry.settings
309 311 config.set_request_factory(Request)
310 312
311 313 # plugin information
312 314 config.registry.rhodecode_plugins = collections.OrderedDict()
313 315
314 316 config.add_directive(
315 317 'register_rhodecode_plugin', register_rhodecode_plugin)
316 318
317 319 config.add_directive('configure_celery', configure_celery)
318 320
319 321 if settings.get('appenlight', False):
320 322 config.include('appenlight_client.ext.pyramid_tween')
321 323
322 324 load_all = should_load_all()
323 325
324 326 # Includes which are required. The application would fail without them.
325 327 config.include('pyramid_mako')
326 328 config.include('rhodecode.lib.rc_beaker')
327 329 config.include('rhodecode.lib.rc_cache')
328 330 config.include('rhodecode.lib.rc_cache.archive_cache')
329 331
330 332 config.include('rhodecode.apps._base.navigation')
331 333 config.include('rhodecode.apps._base.subscribers')
332 334 config.include('rhodecode.tweens')
333 335 config.include('rhodecode.authentication')
334 336
335 337 if load_all:
336 338
337 339 # load CE authentication plugins
338 340
339 341 if auth_resources:
340 342 ce_auth_resources.extend(auth_resources)
341 343
342 344 for resource in ce_auth_resources:
343 345 config.include(resource)
344 346
345 347 # Auto discover authentication plugins and include their configuration.
346 348 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
347 349 from rhodecode.authentication import discover_legacy_plugins
348 350 discover_legacy_plugins(config)
349 351
350 352 # apps
351 353 if load_all:
352 354 log.debug('Starting config.include() calls')
353 355 config.include('rhodecode.api.includeme')
354 356 config.include('rhodecode.apps._base.includeme')
355 357 config.include('rhodecode.apps._base.navigation.includeme')
356 358 config.include('rhodecode.apps._base.subscribers.includeme')
357 359 config.include('rhodecode.apps.hovercards.includeme')
358 360 config.include('rhodecode.apps.ops.includeme')
359 361 config.include('rhodecode.apps.channelstream.includeme')
360 362 config.include('rhodecode.apps.file_store.includeme')
361 363 config.include('rhodecode.apps.admin.includeme')
362 364 config.include('rhodecode.apps.login.includeme')
363 365 config.include('rhodecode.apps.home.includeme')
364 366 config.include('rhodecode.apps.journal.includeme')
365 367
366 368 config.include('rhodecode.apps.repository.includeme')
367 369 config.include('rhodecode.apps.repo_group.includeme')
368 370 config.include('rhodecode.apps.user_group.includeme')
369 371 config.include('rhodecode.apps.search.includeme')
370 372 config.include('rhodecode.apps.user_profile.includeme')
371 373 config.include('rhodecode.apps.user_group_profile.includeme')
372 374 config.include('rhodecode.apps.my_account.includeme')
373 375 config.include('rhodecode.apps.gist.includeme')
374 376
375 377 config.include('rhodecode.apps.svn_support.includeme')
376 378 config.include('rhodecode.apps.ssh_support.includeme')
377 379 config.include('rhodecode.apps.debug_style')
378 380
379 381 if load_all:
380 382 config.include('rhodecode.integrations.includeme')
381 383 config.include('rhodecode.integrations.routes.includeme')
382 384
383 385 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
384 386 settings['default_locale_name'] = settings.get('lang', 'en')
385 387 config.add_translation_dirs('rhodecode:i18n/')
386 388
387 389 # Add subscribers.
388 390 if load_all:
389 391 log.debug('Adding subscribers....')
390 392 config.add_subscriber(scan_repositories_if_enabled,
391 393 pyramid.events.ApplicationCreated)
392 394 config.add_subscriber(write_metadata_if_needed,
393 395 pyramid.events.ApplicationCreated)
394 396 config.add_subscriber(write_usage_data,
395 397 pyramid.events.ApplicationCreated)
396 398 config.add_subscriber(write_js_routes_if_enabled,
397 399 pyramid.events.ApplicationCreated)
398 400
399 401
400 402 # Set the default renderer for HTML templates to mako.
401 403 config.add_mako_renderer('.html')
402 404
403 405 config.add_renderer(
404 406 name='json_ext',
405 407 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
406 408
407 409 config.add_renderer(
408 410 name='string_html',
409 411 factory='rhodecode.lib.string_renderer.html')
410 412
411 413 # include RhodeCode plugins
412 414 includes = aslist(settings.get('rhodecode.includes', []))
413 415 log.debug('processing rhodecode.includes data...')
414 416 for inc in includes:
415 417 config.include(inc)
416 418
417 419 # custom not found view, if our pyramid app doesn't know how to handle
418 420 # the request pass it to potential VCS handling ap
419 421 config.add_notfound_view(not_found_view)
420 422 if not settings.get('debugtoolbar.enabled', False):
421 423 # disabled debugtoolbar handle all exceptions via the error_handlers
422 424 config.add_view(error_handler, context=Exception)
423 425
424 426 # all errors including 403/404/50X
425 427 config.add_view(error_handler, context=HTTPError)
426 428
427 429
428 430 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
429 431 """
430 432 Apply outer WSGI middlewares around the application.
431 433 """
432 434 registry = config.registry
433 435 settings = registry.settings
434 436
435 437 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
436 438 pyramid_app = HttpsFixup(pyramid_app, settings)
437 439
438 440 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
439 441 pyramid_app, settings)
440 442 registry.ae_client = _ae_client
441 443
442 444 if settings['gzip_responses']:
443 445 pyramid_app = make_gzip_middleware(
444 446 pyramid_app, settings, compress_level=1)
445 447
446 448 # this should be the outer most middleware in the wsgi stack since
447 449 # middleware like Routes make database calls
448 450 def pyramid_app_with_cleanup(environ, start_response):
449 451 start = time.time()
450 452 try:
451 453 return pyramid_app(environ, start_response)
452 454 finally:
453 455 # Dispose current database session and rollback uncommitted
454 456 # transactions.
455 457 meta.Session.remove()
456 458
457 459 # In a single threaded mode server, on non sqlite db we should have
458 460 # '0 Current Checked out connections' at the end of a request,
459 461 # if not, then something, somewhere is leaving a connection open
460 462 pool = meta.get_engine().pool
461 463 log.debug('sa pool status: %s', pool.status())
462 464 total = time.time() - start
463 465 log.debug('Request processing finalized: %.4fs', total)
464 466
465 467 return pyramid_app_with_cleanup
466 468
467 469
468 470 def sanitize_settings_and_apply_defaults(global_config, settings):
469 471 """
470 472 Applies settings defaults and does all type conversion.
471 473
472 474 We would move all settings parsing and preparation into this place, so that
473 475 we have only one place left which deals with this part. The remaining parts
474 476 of the application would start to rely fully on well prepared settings.
475 477
476 478 This piece would later be split up per topic to avoid a big fat monster
477 479 function.
478 480 """
479 481
480 482 global_settings_maker = SettingsMaker(global_config)
481 483 global_settings_maker.make_setting('debug', default=False, parser='bool')
482 484 debug_enabled = asbool(global_config.get('debug'))
483 485
484 486 settings_maker = SettingsMaker(settings)
485 487
486 488 settings_maker.make_setting(
487 489 'logging.autoconfigure',
488 490 default=False,
489 491 parser='bool')
490 492
491 493 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
492 494 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
493 495
494 496 # Default includes, possible to change as a user
495 497 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
496 498 log.debug(
497 499 "Using the following pyramid.includes: %s",
498 500 pyramid_includes)
499 501
500 502 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
501 503 settings_maker.make_setting('rhodecode.edition_id', 'CE')
502 504
503 505 if 'mako.default_filters' not in settings:
504 506 # set custom default filters if we don't have it defined
505 507 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
506 508 settings['mako.default_filters'] = 'h_filter'
507 509
508 510 if 'mako.directories' not in settings:
509 511 mako_directories = settings.setdefault('mako.directories', [
510 512 # Base templates of the original application
511 513 'rhodecode:templates',
512 514 ])
513 515 log.debug(
514 516 "Using the following Mako template directories: %s",
515 517 mako_directories)
516 518
517 519 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
518 520 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
519 521 raw_url = settings['beaker.session.url']
520 522 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
521 523 settings['beaker.session.url'] = 'redis://' + raw_url
522 524
523 525 settings_maker.make_setting('__file__', global_config.get('__file__'))
524 526
525 527 # TODO: johbo: Re-think this, usually the call to config.include
526 528 # should allow to pass in a prefix.
527 529 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
528 530
529 531 # Sanitize generic settings.
530 532 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
531 533 settings_maker.make_setting('is_test', False, parser='bool')
532 534 settings_maker.make_setting('gzip_responses', False, parser='bool')
533 535
534 536 # statsd
535 537 settings_maker.make_setting('statsd.enabled', False, parser='bool')
536 538 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
537 539 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
538 540 settings_maker.make_setting('statsd.statsd_prefix', '')
539 541 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
540 542
541 543 settings_maker.make_setting('vcs.svn.compatible_version', '')
542 544 settings_maker.make_setting('vcs.hooks.protocol', 'http')
543 545 settings_maker.make_setting('vcs.hooks.host', '*')
544 546 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
545 547 settings_maker.make_setting('vcs.server', '')
546 548 settings_maker.make_setting('vcs.server.protocol', 'http')
547 549 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
548 550 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
549 551 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
550 552 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
551 553 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
552 554 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
553 555
554 556 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
555 557
556 558 # Support legacy values of vcs.scm_app_implementation. Legacy
557 559 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
558 560 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
559 561 scm_app_impl = settings['vcs.scm_app_implementation']
560 562 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
561 563 settings['vcs.scm_app_implementation'] = 'http'
562 564
563 565 settings_maker.make_setting('appenlight', False, parser='bool')
564 566
565 567 temp_store = tempfile.gettempdir()
566 568 tmp_cache_dir = os.path.join(temp_store, 'rc_cache')
567 569
568 570 # save default, cache dir, and use it for all backends later.
569 571 default_cache_dir = settings_maker.make_setting(
570 572 'cache_dir',
571 573 default=tmp_cache_dir, default_when_empty=True,
572 574 parser='dir:ensured')
573 575
574 576 # exception store cache
575 577 settings_maker.make_setting(
576 578 'exception_tracker.store_path',
577 579 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
578 580 parser='dir:ensured'
579 581 )
580 582
581 583 settings_maker.make_setting(
582 584 'celerybeat-schedule.path',
583 585 default=os.path.join(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
584 586 parser='file:ensured'
585 587 )
586 588
587 589 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
588 590 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
589 591
590 592 # cache_general
591 593 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
592 594 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
593 595 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_general.db'))
594 596
595 597 # cache_perms
596 598 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
597 599 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
598 600 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_perms_db'))
599 601
600 602 # cache_repo
601 603 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
602 604 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
603 605 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_repo_db'))
604 606
605 607 # cache_license
606 608 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
607 609 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
608 610 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_license_db'))
609 611
610 612 # cache_repo_longterm memory, 96H
611 613 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
612 614 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
613 615 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
614 616
615 617 # sql_cache_short
616 618 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
617 619 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
618 620 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
619 621
620 622 # archive_cache
621 623 settings_maker.make_setting('archive_cache.store_dir', os.path.join(default_cache_dir, 'archive_cache'), default_when_empty=True,)
622 624 settings_maker.make_setting('archive_cache.cache_size_gb', 10, parser='float')
623 625 settings_maker.make_setting('archive_cache.cache_shards', 10, parser='int')
624 626
625 627 settings_maker.env_expand()
626 628
627 629 # configure instance id
628 630 config_utils.set_instance_id(settings)
629 631
630 632 return settings
@@ -1,268 +1,263 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22 from rhodecode.model.db import Integration
23 23 from rhodecode.model.meta import Session
24 24 from rhodecode.integrations import integration_type_registry
25
26
27 def route_path(name, **kwargs):
28 return {
29 'home': '/',
30 }[name].format(**kwargs)
25 from rhodecode.tests.routes import route_path
31 26
32 27
33 28 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
34 29 admin_view):
35 30 """
36 31 Posts form data to create integration at the url given then deletes it and
37 32 checks if the redirect url is correct.
38 33 """
39 34 repo_name = repo.repo_name
40 35 repo_group_name = repo_group.group_name
41 36 app.post(url, params={}, status=403) # missing csrf check
42 37 response = app.post(url, params={'csrf_token': csrf_token})
43 38 assert response.status_code == 200
44 39 response.mustcontain('Errors exist')
45 40
46 41 scopes_destinations = [
47 42 ('global',
48 43 ADMIN_PREFIX + '/integrations'),
49 44 ('root-repos',
50 45 ADMIN_PREFIX + '/integrations'),
51 46 ('repo:%s' % repo_name,
52 47 '/%s/settings/integrations' % repo_name),
53 48 ('repogroup:%s' % repo_group_name,
54 49 '/%s/_settings/integrations' % repo_group_name),
55 50 ('repogroup-recursive:%s' % repo_group_name,
56 51 '/%s/_settings/integrations' % repo_group_name),
57 52 ]
58 53
59 54 for scope, destination in scopes_destinations:
60 55 if admin_view:
61 56 destination = ADMIN_PREFIX + '/integrations'
62 57
63 58 form_data = [
64 59 ('csrf_token', csrf_token),
65 60 ('__start__', 'options:mapping'),
66 61 ('name', 'test integration'),
67 62 ('scope', scope),
68 63 ('enabled', 'true'),
69 64 ('__end__', 'options:mapping'),
70 65 ('__start__', 'settings:mapping'),
71 66 ('test_int_field', '34'),
72 67 ('test_string_field', ''), # empty value on purpose as it's required
73 68 ('__end__', 'settings:mapping'),
74 69 ]
75 70 errors_response = app.post(url, form_data)
76 71 assert 'Errors exist' in errors_response.text
77 72
78 73 form_data[-2] = ('test_string_field', 'data!')
79 74 assert Session().query(Integration).count() == 0
80 75 created_response = app.post(url, form_data)
81 76 assert Session().query(Integration).count() == 1
82 77
83 78 delete_response = app.post(
84 79 created_response.location,
85 80 params={'csrf_token': csrf_token, 'delete': 'delete'})
86 81
87 82 assert Session().query(Integration).count() == 0
88 83 assert delete_response.location.endswith(destination)
89 84
90 85
91 86
92 87 @pytest.mark.usefixtures('app', 'autologin_user')
93 88 class TestIntegrationsView(object):
94 89 pass
95 90
96 91
97 92 class TestGlobalIntegrationsView(TestIntegrationsView):
98 93 def test_index_no_integrations(self):
99 94 url = ADMIN_PREFIX + '/integrations'
100 95 response = self.app.get(url)
101 96
102 97 assert response.status_code == 200
103 98 response.mustcontain('exist yet')
104 99
105 100 def test_index_with_integrations(self, global_integration_stub):
106 101 url = ADMIN_PREFIX + '/integrations'
107 102 response = self.app.get(url)
108 103
109 104 assert response.status_code == 200
110 105 response.mustcontain(no=['exist yet'])
111 106 response.mustcontain(global_integration_stub.name)
112 107
113 108 @pytest.mark.parametrize(
114 109 'IntegrationType', integration_type_registry.values())
115 110 def test_new_integration_page(self, IntegrationType):
116 111 url = ADMIN_PREFIX + '/integrations/new'
117 112
118 113 response = self.app.get(url, status=200)
119 114 if not IntegrationType.is_dummy:
120 115 url = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
121 116 integration=IntegrationType.key)
122 117 response.mustcontain(url)
123 118
124 119 @pytest.mark.parametrize(
125 120 'IntegrationType', integration_type_registry.values())
126 121 def test_get_create_integration_page(self, IntegrationType):
127 122 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
128 123 integration_key=IntegrationType.key)
129 124 if IntegrationType.is_dummy:
130 125 self.app.get(url, status=404)
131 126 else:
132 127 response = self.app.get(url, status=200)
133 128 response.mustcontain(IntegrationType.display_name)
134 129
135 130 def test_post_integration_page(self, StubIntegrationType, csrf_token,
136 131 test_repo_group, backend_random):
137 132 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
138 133 integration_key=StubIntegrationType.key)
139 134
140 135 _post_integration_test_helper(
141 136 self.app, url, csrf_token, admin_view=True,
142 137 repo=backend_random.repo, repo_group=test_repo_group)
143 138
144 139
145 140 class TestRepoIntegrationsView(TestIntegrationsView):
146 141 def test_index_no_integrations(self, backend_random):
147 142 url = '/{repo_name}/settings/integrations'.format(
148 143 repo_name=backend_random.repo.repo_name)
149 144 response = self.app.get(url)
150 145
151 146 assert response.status_code == 200
152 147 response.mustcontain('exist yet')
153 148
154 149 def test_index_with_integrations(self, repo_integration_stub):
155 150 url = '/{repo_name}/settings/integrations'.format(
156 151 repo_name=repo_integration_stub.repo.repo_name)
157 152 stub_name = repo_integration_stub.name
158 153
159 154 response = self.app.get(url)
160 155
161 156 assert response.status_code == 200
162 157 response.mustcontain(stub_name)
163 158 response.mustcontain(no=['exist yet'])
164 159
165 160 @pytest.mark.parametrize(
166 161 'IntegrationType', integration_type_registry.values())
167 162 def test_new_integration_page(self, backend_random, IntegrationType):
168 163 repo_name = backend_random.repo.repo_name
169 164 url = '/{repo_name}/settings/integrations/new'.format(
170 165 repo_name=repo_name)
171 166
172 167 response = self.app.get(url, status=200)
173 168
174 169 url = '/{repo_name}/settings/integrations/{integration}/new'.format(
175 170 repo_name=repo_name,
176 171 integration=IntegrationType.key)
177 172 if not IntegrationType.is_dummy:
178 173 response.mustcontain(url)
179 174
180 175 @pytest.mark.parametrize(
181 176 'IntegrationType', integration_type_registry.values())
182 177 def test_get_create_integration_page(self, backend_random, IntegrationType):
183 178 repo_name = backend_random.repo.repo_name
184 179 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
185 180 repo_name=repo_name, integration_key=IntegrationType.key)
186 181 if IntegrationType.is_dummy:
187 182 self.app.get(url, status=404)
188 183 else:
189 184 response = self.app.get(url, status=200)
190 185 response.mustcontain(IntegrationType.display_name)
191 186
192 187 def test_post_integration_page(self, backend_random, test_repo_group,
193 188 StubIntegrationType, csrf_token):
194 189 repo_name = backend_random.repo.repo_name
195 190 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
196 191 repo_name=repo_name, integration_key=StubIntegrationType.key)
197 192
198 193 _post_integration_test_helper(
199 194 self.app, url, csrf_token, admin_view=False,
200 195 repo=backend_random.repo, repo_group=test_repo_group)
201 196
202 197
203 198 class TestRepoGroupIntegrationsView(TestIntegrationsView):
204 199 def test_index_no_integrations(self, test_repo_group):
205 200 url = '/{repo_group_name}/_settings/integrations'.format(
206 201 repo_group_name=test_repo_group.group_name)
207 202 response = self.app.get(url)
208 203
209 204 assert response.status_code == 200
210 205 response.mustcontain('exist yet')
211 206
212 207 def test_index_with_integrations(
213 208 self, test_repo_group, repogroup_integration_stub):
214 209
215 210 url = '/{repo_group_name}/_settings/integrations'.format(
216 211 repo_group_name=test_repo_group.group_name)
217 212
218 213 stub_name = repogroup_integration_stub.name
219 214 response = self.app.get(url)
220 215
221 216 assert response.status_code == 200
222 217 response.mustcontain(no=['exist yet'])
223 218 response.mustcontain(stub_name)
224 219
225 220 def test_new_integration_page(self, test_repo_group):
226 221 repo_group_name = test_repo_group.group_name
227 222 url = '/{repo_group_name}/_settings/integrations/new'.format(
228 223 repo_group_name=test_repo_group.group_name)
229 224
230 225 response = self.app.get(url)
231 226
232 227 assert response.status_code == 200
233 228
234 229 for integration_key, integration_obj in integration_type_registry.items():
235 230 if not integration_obj.is_dummy:
236 231 nurl = (
237 232 '/{repo_group_name}/_settings/integrations/{integration}/new').format(
238 233 repo_group_name=repo_group_name,
239 234 integration=integration_key)
240 235 response.mustcontain(nurl)
241 236
242 237 @pytest.mark.parametrize(
243 238 'IntegrationType', integration_type_registry.values())
244 239 def test_get_create_integration_page(
245 240 self, test_repo_group, IntegrationType):
246 241
247 242 repo_group_name = test_repo_group.group_name
248 243 url = ('/{repo_group_name}/_settings/integrations/{integration_key}/new'
249 244 ).format(repo_group_name=repo_group_name,
250 245 integration_key=IntegrationType.key)
251 246
252 247 if not IntegrationType.is_dummy:
253 248 response = self.app.get(url, status=200)
254 249 response.mustcontain(IntegrationType.display_name)
255 250 else:
256 251 self.app.get(url, status=404)
257 252
258 253 def test_post_integration_page(self, test_repo_group, backend_random,
259 254 StubIntegrationType, csrf_token):
260 255
261 256 repo_group_name = test_repo_group.group_name
262 257 url = ('/{repo_group_name}/_settings/integrations/{integration_key}/new'
263 258 ).format(repo_group_name=repo_group_name,
264 259 integration_key=StubIntegrationType.key)
265 260
266 261 _post_integration_test_helper(
267 262 self.app, url, csrf_token, admin_view=False,
268 263 repo=backend_random.repo, repo_group=test_repo_group)
@@ -1,412 +1,382 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 pyroutes.register('admin_artifacts', '/_admin/artifacts', []);
16 pyroutes.register('admin_artifacts_data', '/_admin/artifacts-data', []);
17 pyroutes.register('admin_artifacts_delete', '/_admin/artifacts/%(uid)s/delete', ['uid']);
18 pyroutes.register('admin_artifacts_show_all', '/_admin/artifacts', []);
19 pyroutes.register('admin_artifacts_show_info', '/_admin/artifacts/%(uid)s', ['uid']);
20 pyroutes.register('admin_artifacts_update', '/_admin/artifacts/%(uid)s/update', ['uid']);
15 pyroutes.register('admin_artifacts', '/_admin/_admin/artifacts', []);
16 pyroutes.register('admin_artifacts_delete', '/_admin/_admin/artifacts/%(uid)s/delete', ['uid']);
17 pyroutes.register('admin_artifacts_show_all', '/_admin/_admin/artifacts', []);
18 pyroutes.register('admin_artifacts_show_info', '/_admin/_admin/artifacts/%(uid)s', ['uid']);
19 pyroutes.register('admin_artifacts_update', '/_admin/_admin/artifacts/%(uid)s/update', ['uid']);
21 20 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
22 21 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
23 pyroutes.register('admin_automation', '/_admin/automation', []);
24 pyroutes.register('admin_automation_update', '/_admin/automation/%(entry_id)s/update', ['entry_id']);
22 pyroutes.register('admin_automation', '/_admin/_admin/automation', []);
25 23 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
26 24 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
27 25 pyroutes.register('admin_home', '/_admin', []);
28 26 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
29 27 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
30 28 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
31 29 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
32 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
33 30 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
34 31 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
35 32 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
36 33 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
37 34 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
38 35 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
39 36 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
40 37 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
41 38 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
42 pyroutes.register('admin_scheduler', '/_admin/scheduler', []);
43 pyroutes.register('admin_scheduler_show_tasks', '/_admin/scheduler/_tasks', []);
39 pyroutes.register('admin_scheduler', '/_admin/_admin/scheduler', []);
44 40 pyroutes.register('admin_settings', '/_admin/settings', []);
45 41 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
46 42 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
47 43 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
48 44 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
49 45 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions_delete_all', []);
50 46 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 47 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
52 48 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
53 49 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
54 50 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
55 51 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
56 52 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
57 53 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
58 54 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
59 55 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
60 56 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
61 57 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
62 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
63 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
64 58 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
65 59 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
66 60 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
67 61 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
68 62 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
69 63 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
70 64 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
71 pyroutes.register('admin_settings_scheduler_create', '/_admin/scheduler/create', []);
72 pyroutes.register('admin_settings_scheduler_delete', '/_admin/scheduler/%(schedule_id)s/delete', ['schedule_id']);
73 pyroutes.register('admin_settings_scheduler_edit', '/_admin/scheduler/%(schedule_id)s', ['schedule_id']);
74 pyroutes.register('admin_settings_scheduler_execute', '/_admin/scheduler/%(schedule_id)s/execute', ['schedule_id']);
75 pyroutes.register('admin_settings_scheduler_new', '/_admin/scheduler/new', []);
76 pyroutes.register('admin_settings_scheduler_update', '/_admin/scheduler/%(schedule_id)s/update', ['schedule_id']);
77 65 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
78 66 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
79 67 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
80 68 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
81 69 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
82 70 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
83 71 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
84 72 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
85 73 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
86 74 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
87 75 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
88 76 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
89 77 pyroutes.register('apiv2', '/_admin/api', []);
90 78 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
91 79 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
92 80 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
93 81 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
94 82 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
95 83 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
96 84 pyroutes.register('channelstream_proxy', '/_channelstream', []);
97 85 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
98 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
99 86 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
100 87 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
101 88 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
102 89 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
103 90 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
104 91 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
105 92 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
106 93 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
107 94 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
108 95 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
109 96 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
110 97 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
111 98 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
112 99 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
113 100 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
114 101 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
115 102 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
116 103 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
117 104 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
118 105 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
119 106 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
120 107 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
121 108 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
122 109 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
123 110 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
124 111 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
125 112 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
126 113 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
127 114 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
128 115 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
129 116 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
130 117 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
131 118 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
132 119 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
133 120 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
134 121 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
135 122 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
136 123 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
137 124 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
138 125 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
139 126 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
140 127 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
141 128 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
142 129 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
143 130 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
144 131 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
145 132 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
146 133 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
147 134 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
148 135 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
149 136 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
150 137 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
151 138 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
152 139 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
153 140 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
154 141 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
155 142 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
156 143 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
157 144 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
158 145 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
159 146 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
160 147 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
161 148 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
162 149 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
163 150 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
164 151 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
165 152 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
166 153 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
167 154 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
168 155 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
169 156 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
170 157 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
171 158 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
172 159 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
173 160 pyroutes.register('favicon', '/favicon.ico', []);
174 161 pyroutes.register('file_preview', '/_file_preview', []);
175 162 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
176 163 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
177 164 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
178 165 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
179 166 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
180 167 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/rev/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
181 168 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/rev/%(revision)s', ['gist_id', 'revision']);
182 169 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
183 170 pyroutes.register('gists_create', '/_admin/gists/create', []);
184 171 pyroutes.register('gists_new', '/_admin/gists/new', []);
185 172 pyroutes.register('gists_show', '/_admin/gists', []);
186 173 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
187 174 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
188 175 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
189 176 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
190 177 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
191 178 pyroutes.register('goto_switcher_data', '/_goto_data', []);
192 179 pyroutes.register('home', '/', []);
193 180 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
194 181 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
195 182 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
196 183 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
197 184 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
198 185 pyroutes.register('journal', '/_admin/journal', []);
199 186 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
200 187 pyroutes.register('journal_public', '/_admin/public_journal', []);
201 188 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
202 189 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
203 190 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
204 191 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
205 192 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
206 193 pyroutes.register('login', '/_admin/login', []);
207 194 pyroutes.register('logout', '/_admin/logout', []);
208 195 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
209 196 pyroutes.register('main_page_repos_data', '/_home_repos', []);
210 197 pyroutes.register('markup_preview', '/_markup_preview', []);
211 198 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
212 199 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
213 200 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
214 201 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
215 202 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
216 203 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
217 204 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
218 205 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
219 206 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
220 207 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
221 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
222 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
223 208 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
224 209 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
225 210 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
226 211 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
227 212 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
228 213 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
229 214 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
230 215 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
231 216 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
232 217 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
233 218 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
234 219 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
235 220 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
236 221 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
237 222 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
238 223 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
239 224 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
240 225 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
241 226 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
242 227 pyroutes.register('notifications_mark_all_read', '/_admin/notifications_mark_all_read', []);
243 228 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
244 229 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
245 230 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
246 231 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
247 232 pyroutes.register('ops_healthcheck', '/_admin/ops/status', []);
248 233 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
249 234 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
250 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
251 235 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
252 236 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
253 237 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
254 238 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
255 239 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
256 240 pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
257 241 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
258 242 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
259 243 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
260 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
261 244 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
262 245 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
263 246 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
264 247 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
265 248 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
266 249 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
267 250 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
268 251 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
269 252 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
270 253 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
271 254 pyroutes.register('register', '/_admin/register', []);
272 255 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
273 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
274 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
275 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
276 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
277 256 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
278 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
279 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
280 pyroutes.register('repo_artifacts_stream_script', '/_file_store/stream-upload-script', []);
281 pyroutes.register('repo_artifacts_stream_store', '/_file_store/stream-upload', []);
282 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
283 257 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
284 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
285 258 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
286 259 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
287 260 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
288 261 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
289 262 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
290 263 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
291 264 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
292 265 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
293 266 pyroutes.register('repo_commit_comment_history_view', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/history_view/%(comment_history_id)s', ['repo_name', 'commit_id', 'comment_id', 'comment_history_id']);
294 267 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
295 268 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
296 269 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
297 270 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
298 271 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
299 272 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
300 273 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
301 274 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
302 275 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
303 276 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
304 277 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
305 278 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
306 279 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
307 280 pyroutes.register('repo_create', '/_admin/repos/create', []);
308 281 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
309 282 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
310 283 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
311 284 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
312 285 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
313 286 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
314 287 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
315 288 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
316 289 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
317 290 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
318 291 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
319 292 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
320 293 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
321 294 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
322 295 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
323 296 pyroutes.register('repo_files_check_head', '/%(repo_name)s/check_head/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
324 297 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
325 298 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
326 299 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
327 300 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
328 301 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
329 302 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
330 303 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
331 304 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
332 305 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
333 306 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
334 307 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
335 308 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
336 309 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
337 310 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
338 311 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
339 312 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
340 313 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
341 314 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
342 315 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
343 316 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
344 317 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
345 318 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
346 319 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
347 320 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
348 321 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
349 322 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
350 323 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
351 324 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
352 325 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
353 326 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
354 327 pyroutes.register('repo_list_data', '/_repos', []);
355 328 pyroutes.register('repo_new', '/_admin/repos/new', []);
356 329 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
357 330 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
358 331 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
359 332 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
360 333 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
361 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
362 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
363 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
364 334 pyroutes.register('repo_settings_quick_actions', '/%(repo_name)s/settings/quick-action', ['repo_name']);
365 335 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
366 336 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
367 337 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
368 338 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
369 339 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
370 340 pyroutes.register('repos', '/_admin/repos', []);
371 341 pyroutes.register('repos_data', '/_admin/repos_data', []);
372 342 pyroutes.register('reset_password', '/_admin/password_reset', []);
373 343 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
374 344 pyroutes.register('robots', '/robots.txt', []);
375 345 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
376 346 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
377 347 pyroutes.register('search', '/_admin/search', []);
378 348 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
379 349 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
380 350 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
381 351 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
382 352 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
383 353 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
384 354 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
385 355 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
386 356 pyroutes.register('upload_file', '/_file_store/upload', []);
387 357 pyroutes.register('user_autocomplete_data', '/_users', []);
388 358 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
389 359 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
390 360 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
391 361 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
392 362 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
393 363 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
394 364 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
395 365 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
396 366 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
397 367 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
398 368 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
399 369 pyroutes.register('user_groups', '/_admin/user_groups', []);
400 370 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
401 371 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
402 372 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
403 373 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
404 374 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
405 375 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
406 376 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
407 377 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
408 378 pyroutes.register('users', '/_admin/users', []);
409 379 pyroutes.register('users_create', '/_admin/users/create', []);
410 380 pyroutes.register('users_data', '/_admin/users_data', []);
411 381 pyroutes.register('users_new', '/_admin/users/new', []);
412 382 }
@@ -1,252 +1,244 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21 import time
22 22 import logging
23 23 import datetime
24 24 import tempfile
25 25 from os.path import join as jn
26 26 import urllib.parse
27 27
28 28 import pytest
29 29
30 30 from rhodecode.model.db import User
31 31 from rhodecode.lib import auth
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib.helpers import flash
34 34 from rhodecode.lib.str_utils import safe_str
35 35 from rhodecode.lib.hash_utils import sha1_safe
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 __all__ = [
40 'get_new_dir', 'TestController', 'route_path_generator',
40 'get_new_dir', 'TestController',
41 41 'clear_cache_regions',
42 42 'assert_session_flash', 'login_user', 'no_newline_id_generator',
43 43 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'SVN_REPO',
44 44 'NEW_HG_REPO', 'NEW_GIT_REPO',
45 45 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
46 46 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
47 47 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
48 48 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO',
49 49 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
50 50 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'SCM_TESTS',
51 51 ]
52 52
53 53
54 54 # SOME GLOBALS FOR TESTS
55 55 TEST_DIR = tempfile.gettempdir()
56 56
57 57 TESTS_TMP_PATH = jn(TEST_DIR, 'rc_test_{}'.format(next(tempfile._RandomNameSequence())))
58 58 TEST_USER_ADMIN_LOGIN = 'test_admin'
59 59 TEST_USER_ADMIN_PASS = 'test12'
60 60 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
61 61
62 62 TEST_USER_REGULAR_LOGIN = 'test_regular'
63 63 TEST_USER_REGULAR_PASS = 'test12'
64 64 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
65 65
66 66 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
67 67 TEST_USER_REGULAR2_PASS = 'test12'
68 68 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
69 69
70 70 HG_REPO = 'vcs_test_hg'
71 71 GIT_REPO = 'vcs_test_git'
72 72 SVN_REPO = 'vcs_test_svn'
73 73
74 74 NEW_HG_REPO = 'vcs_test_hg_new'
75 75 NEW_GIT_REPO = 'vcs_test_git_new'
76 76
77 77 HG_FORK = 'vcs_test_hg_fork'
78 78 GIT_FORK = 'vcs_test_git_fork'
79 79
80 80 ## VCS
81 81 SCM_TESTS = ['hg', 'git']
82 82 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
83 83
84 84 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
85 85 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, f'vcsgitclone{uniq_suffix}')
86 86 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, f'vcsgitpull{uniq_suffix}')
87 87
88 88 TEST_HG_REPO = jn(TESTS_TMP_PATH, HG_REPO)
89 89 TEST_HG_REPO_CLONE = jn(TESTS_TMP_PATH, f'vcshgclone{uniq_suffix}')
90 90 TEST_HG_REPO_PULL = jn(TESTS_TMP_PATH, f'vcshgpull{uniq_suffix}')
91 91
92 92 TEST_REPO_PREFIX = 'vcs-test'
93 93
94 94
95 95 def clear_cache_regions(regions=None):
96 96 # dogpile
97 97 from rhodecode.lib.rc_cache import region_meta
98 98 for region_name, region in region_meta.dogpile_cache_regions.items():
99 99 if not regions or region_name in regions:
100 100 region.invalidate()
101 101
102 102
103 103 def get_new_dir(title):
104 104 """
105 105 Returns always new directory path.
106 106 """
107 107 from rhodecode.tests.vcs.utils import get_normalized_path
108 108 name_parts = [TEST_REPO_PREFIX]
109 109 if title:
110 110 name_parts.append(title)
111 111 hex_str = sha1_safe(f'{os.getpid()} {time.time()}')
112 112 name_parts.append(hex_str)
113 113 name = '-'.join(name_parts)
114 114 path = os.path.join(TEST_DIR, name)
115 115 return get_normalized_path(path)
116 116
117 117
118 118 def repo_id_generator(name):
119 119 numeric_hash = 0
120 120 for char in name:
121 121 numeric_hash += (ord(char))
122 122 return numeric_hash
123 123
124 124
125 125 @pytest.mark.usefixtures('app', 'index_location')
126 126 class TestController(object):
127 127
128 128 maxDiff = None
129 129
130 130 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
131 131 password=TEST_USER_ADMIN_PASS):
132 132 self._logged_username = username
133 133 self._session = login_user_session(self.app, username, password)
134 134 self.csrf_token = auth.get_csrf_token(self._session)
135 135
136 136 return self._session['rhodecode_user']
137 137
138 138 def logout_user(self):
139 139 logout_user_session(self.app, auth.get_csrf_token(self._session))
140 140 self.csrf_token = None
141 141 self._logged_username = None
142 142 self._session = None
143 143
144 144 def _get_logged_user(self):
145 145 return User.get_by_username(self._logged_username)
146 146
147 147
148 148 def login_user_session(
149 149 app, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS):
150 150
151 151 response = app.post(
152 152 h.route_path('login'),
153 153 {'username': username, 'password': password})
154 154 if 'invalid user name' in response.text:
155 155 pytest.fail(f'could not login using {username} {password}')
156 156
157 157 assert response.status == '302 Found'
158 158 response = response.follow()
159 159 assert response.status == '200 OK'
160 160
161 161 session = response.get_session_from_response()
162 162 assert 'rhodecode_user' in session
163 163 rc_user = session['rhodecode_user']
164 164 assert rc_user.get('username') == username
165 165 assert rc_user.get('is_authenticated')
166 166
167 167 return session
168 168
169 169
170 170 def logout_user_session(app, csrf_token):
171 171 app.post(h.route_path('logout'), {'csrf_token': csrf_token}, status=302)
172 172
173 173
174 174 def login_user(app, username=TEST_USER_ADMIN_LOGIN,
175 175 password=TEST_USER_ADMIN_PASS):
176 176 return login_user_session(app, username, password)['rhodecode_user']
177 177
178 178
179 179 def assert_session_flash(response, msg=None, category=None, no_=None):
180 180 """
181 181 Assert on a flash message in the current session.
182 182
183 183 :param response: Response from give calll, it will contain flash
184 184 messages or bound session with them.
185 185 :param msg: The expected message. Will be evaluated if a
186 186 :class:`LazyString` is passed in.
187 187 :param category: Optional. If passed, the message category will be
188 188 checked as well.
189 189 :param no_: Optional. If passed, the message will be checked to NOT
190 190 be in the flash session
191 191 """
192 192 if msg is None and no_ is None:
193 193 raise ValueError("Parameter msg or no_ is required.")
194 194
195 195 if msg and no_:
196 196 raise ValueError("Please specify either msg or no_, but not both")
197 197
198 198 session = response.get_session_from_response()
199 199 messages = flash.pop_messages(session=session)
200 200 msg = _eval_if_lazy(msg)
201 201
202 202 if no_:
203 203 error_msg = f'unable to detect no_ message `{no_}` in empty flash list'
204 204 else:
205 205 error_msg = f'unable to find message `{msg}` in empty flash list'
206 206 assert messages, error_msg
207 207 message = messages[0]
208 208
209 209 message_text = _eval_if_lazy(message.message) or ''
210 210
211 211 if no_:
212 212 if no_ in message_text:
213 213 msg = f'msg `{no_}` found in session flash.'
214 214 pytest.fail(safe_str(msg))
215 215 else:
216 216
217 217 if msg not in message_text:
218 218 fail_msg = f'msg `{msg}` not found in ' \
219 219 f'session flash: got `{message_text}` (type:{type(message_text)}) instead'
220 220
221 221 pytest.fail(safe_str(fail_msg))
222 222
223 223 if category:
224 224 assert category == message.category
225 225
226 226
227 227 def _eval_if_lazy(value):
228 228 return value.eval() if hasattr(value, 'eval') else value
229 229
230 230
231 231 def no_newline_id_generator(test_name):
232 232 """
233 233 Generates a test name without spaces or newlines characters. Used for
234 234 nicer output of progress of test
235 235 """
236 236
237 237 test_name = safe_str(test_name)\
238 238 .replace('\n', '_N') \
239 239 .replace('\r', '_N') \
240 240 .replace('\t', '_T') \
241 241 .replace(' ', '_S')
242 242
243 243 return test_name or 'test-with-empty-name'
244 244
245
246 def route_path_generator(url_defs, name, params=None, **kwargs):
247
248 base_url = url_defs[name].format(**kwargs)
249
250 if params:
251 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
252 return base_url
@@ -1,159 +1,132 b''
1 1
2 2
3 3 # Copyright (C) 2016-2023 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
26
27 def route_path(name, params=None, **kwargs):
28 import urllib.request
29 import urllib.parse
30 import urllib.error
31 from rhodecode.apps._base import ADMIN_PREFIX
32
33 base_url = {
34 'home': '/',
35 'admin_home': ADMIN_PREFIX,
36 'repos':
37 ADMIN_PREFIX + '/repos',
38 'repos_data':
39 ADMIN_PREFIX + '/repos_data',
40 'repo_groups':
41 ADMIN_PREFIX + '/repo_groups',
42 'repo_groups_data':
43 ADMIN_PREFIX + '/repo_groups_data',
44 'user_groups':
45 ADMIN_PREFIX + '/user_groups',
46 'user_groups_data':
47 ADMIN_PREFIX + '/user_groups_data',
48 }[name].format(**kwargs)
49
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 return base_url
25 from rhodecode.tests.routes import route_path
53 26
54 27
55 28 fixture = Fixture()
56 29
57 30
58 31 class TestAdminDelegatedUser(TestController):
59 32
60 33 def test_regular_user_cannot_see_admin_interfaces(self, user_util, xhr_header):
61 34 user = user_util.create_user(password='qweqwe')
62 35 user_util.inherit_default_user_permissions(user.username, False)
63 36
64 37 self.log_user(user.username, 'qweqwe')
65 38
66 39 # user doesn't have any access to resources so main admin page should 404
67 40 self.app.get(route_path('admin_home'), status=404)
68 41
69 42 response = self.app.get(route_path('repos_data'),
70 43 status=200, extra_environ=xhr_header)
71 44 assert response.json['data'] == []
72 45
73 46 response = self.app.get(route_path('repo_groups_data'),
74 47 status=200, extra_environ=xhr_header)
75 48 assert response.json['data'] == []
76 49
77 50 response = self.app.get(route_path('user_groups_data'),
78 51 status=200, extra_environ=xhr_header)
79 52 assert response.json['data'] == []
80 53
81 54 def test_regular_user_can_see_admin_interfaces_if_owner(self, user_util, xhr_header):
82 55 user = user_util.create_user(password='qweqwe')
83 56 username = user.username
84 57
85 58 repo = user_util.create_repo(owner=username)
86 59 repo_name = repo.repo_name
87 60
88 61 repo_group = user_util.create_repo_group(owner=username)
89 62 repo_group_name = repo_group.group_name
90 63
91 64 user_group = user_util.create_user_group(owner=username)
92 65 user_group_name = user_group.users_group_name
93 66
94 67 self.log_user(username, 'qweqwe')
95 68
96 69 response = self.app.get(route_path('admin_home'))
97 70
98 71 assert_response = response.assert_response()
99 72
100 73 assert_response.element_contains('td.delegated-admin-repos', '1')
101 74 assert_response.element_contains('td.delegated-admin-repo-groups', '1')
102 75 assert_response.element_contains('td.delegated-admin-user-groups', '1')
103 76
104 77 # admin interfaces have visible elements
105 78 response = self.app.get(route_path('repos_data'),
106 79 extra_environ=xhr_header, status=200)
107 80 response.mustcontain('<a href=\\"/{}\\">'.format(repo_name))
108 81
109 82 response = self.app.get(route_path('repo_groups_data'),
110 83 extra_environ=xhr_header, status=200)
111 84 response.mustcontain('<a href=\\"/{}\\">'.format(repo_group_name))
112 85
113 86 response = self.app.get(route_path('user_groups_data'),
114 87 extra_environ=xhr_header, status=200)
115 88 response.mustcontain('<a href=\\"/_profile_user_group/{}\\">'.format(user_group_name))
116 89
117 90 def test_regular_user_can_see_admin_interfaces_if_admin_perm(
118 91 self, user_util, xhr_header):
119 92 user = user_util.create_user(password='qweqwe')
120 93 username = user.username
121 94
122 95 repo = user_util.create_repo()
123 96 repo_name = repo.repo_name
124 97
125 98 repo_group = user_util.create_repo_group()
126 99 repo_group_name = repo_group.group_name
127 100
128 101 user_group = user_util.create_user_group()
129 102 user_group_name = user_group.users_group_name
130 103
131 104 user_util.grant_user_permission_to_repo(
132 105 repo, user, 'repository.admin')
133 106 user_util.grant_user_permission_to_repo_group(
134 107 repo_group, user, 'group.admin')
135 108 user_util.grant_user_permission_to_user_group(
136 109 user_group, user, 'usergroup.admin')
137 110
138 111 self.log_user(username, 'qweqwe')
139 112 # check if in home view, such user doesn't see the "admin" menus
140 113 response = self.app.get(route_path('admin_home'))
141 114
142 115 assert_response = response.assert_response()
143 116
144 117 assert_response.element_contains('td.delegated-admin-repos', '1')
145 118 assert_response.element_contains('td.delegated-admin-repo-groups', '1')
146 119 assert_response.element_contains('td.delegated-admin-user-groups', '1')
147 120
148 121 # admin interfaces have visible elements
149 122 response = self.app.get(route_path('repos_data'),
150 123 extra_environ=xhr_header, status=200)
151 124 response.mustcontain('<a href=\\"/{}\\">'.format(repo_name))
152 125
153 126 response = self.app.get(route_path('repo_groups_data'),
154 127 extra_environ=xhr_header, status=200)
155 128 response.mustcontain('<a href=\\"/{}\\">'.format(repo_group_name))
156 129
157 130 response = self.app.get(route_path('user_groups_data'),
158 131 extra_environ=xhr_header, status=200)
159 132 response.mustcontain('<a href=\\"/_profile_user_group/{}\\">'.format(user_group_name))
@@ -1,76 +1,71 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
24
25
26 def route_path(name, **kwargs):
27 return {
28 'home': '/',
29 }[name].format(**kwargs)
24 from rhodecode.tests.routes import route_path
30 25
31 26
32 27 class TestSessionBehaviorOnPasswordChange(object):
33 28 @pytest.fixture(autouse=True)
34 29 def patch_password_changed(self, request):
35 30 password_changed_patcher = mock.patch(
36 31 'rhodecode.lib.base.password_changed')
37 32 self.password_changed_mock = password_changed_patcher.start()
38 33 self.password_changed_mock.return_value = False
39 34
40 35 @request.addfinalizer
41 36 def cleanup():
42 37 password_changed_patcher.stop()
43 38
44 39 def test_sessions_are_ok_when_password_is_not_changed(
45 40 self, app, autologin_user):
46 41 response = app.get(route_path('home'))
47 42 assert_response = response.assert_response()
48 43 assert_response.element_contains(
49 44 '#quick_login_link .menu_link_user', TEST_USER_ADMIN_LOGIN)
50 45
51 46 session = response.get_session_from_response()
52 47
53 48 assert 'rhodecode_user' in session
54 49 assert session.was_invalidated is False
55 50
56 51 def test_sessions_invalidated_when_password_is_changed(
57 52 self, app, autologin_user):
58 53 response = app.get(route_path('home'), status=200)
59 54 session = response.get_session_from_response()
60 55
61 56 # now mark as password change
62 57 self.password_changed_mock.return_value = True
63 58
64 59 # flushes session first
65 60 app.get(route_path('home'))
66 61
67 62 # second call is now "different" with flushed empty session
68 63 response = app.get(route_path('home'))
69 64 session = response.get_session_from_response()
70 65
71 66 assert 'rhodecode_user' not in session
72 67
73 68 assert_response = response.assert_response()
74 69 assert_response.element_contains('#quick_login_link .user', 'Sign in')
75 70
76 71
@@ -1,487 +1,486 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 import threading
21 20 import time
22 21 import logging
23 22 import os.path
24 23 import subprocess
25 24 import tempfile
26 25 import urllib.request
27 26 import urllib.error
28 27 import urllib.parse
29 28 from lxml.html import fromstring, tostring
30 29 from lxml.cssselect import CSSSelector
31 30 from urllib.parse import unquote_plus
32 31 import webob
33 32
34 33 from webtest.app import TestResponse, TestApp
35 34 from webtest.compat import print_stderr
36 35
37 36 import pytest
38 37
39 38 try:
40 39 import rc_testdata
41 40 except ImportError:
42 41 raise ImportError('Failed to import rc_testdata, '
43 42 'please make sure this package is installed from requirements_test.txt')
44 43
45 44 from rhodecode.model.db import User, Repository
46 45 from rhodecode.model.meta import Session
47 46 from rhodecode.model.scm import ScmModel
48 47 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
49 48 from rhodecode.lib.vcs.backends.base import EmptyCommit
50 49 from rhodecode.tests import login_user_session
51 50
52 51 log = logging.getLogger(__name__)
53 52
54 53
55 54 class CustomTestResponse(TestResponse):
56 55
57 56 def _save_output(self, out):
58 57 f = tempfile.NamedTemporaryFile(mode='w', delete=False, prefix='rc-test-', suffix='.html')
59 58 f.write(out)
60 59 return f.name
61 60
62 61 def mustcontain(self, *strings, **kw):
63 62 """
64 63 Assert that the response contains all of the strings passed
65 64 in as arguments.
66 65
67 66 Equivalent to::
68 67
69 68 assert string in res
70 69 """
71 70 print_body = kw.pop('print_body', False)
72 71 if 'no' in kw:
73 72 no = kw['no']
74 73 del kw['no']
75 74 if isinstance(no, str):
76 75 no = [no]
77 76 else:
78 77 no = []
79 78 if kw:
80 79 raise TypeError(f"The only keyword argument allowed is 'no' got {kw}")
81 80
82 81 f = self._save_output(str(self))
83 82
84 83 for s in strings:
85 84 if s not in self:
86 85 print_stderr(f"Actual response (no {s!r}):")
87 86 print_stderr(f"body output saved as `{f}`")
88 87 if print_body:
89 88 print_stderr(str(self))
90 89 raise IndexError(f"Body does not contain string {s!r}, body output saved as {f}")
91 90
92 91 for no_s in no:
93 92 if no_s in self:
94 93 print_stderr(f"Actual response (has {no_s!r})")
95 94 print_stderr(f"body output saved as `{f}`")
96 95 if print_body:
97 96 print_stderr(str(self))
98 97 raise IndexError(f"Body contains bad string {no_s!r}, body output saved as {f}")
99 98
100 99 def assert_response(self):
101 100 return AssertResponse(self)
102 101
103 102 def get_session_from_response(self):
104 103 """
105 104 This returns the session from a response object.
106 105 """
107 106 from rhodecode.lib.rc_beaker import session_factory_from_settings
108 107 session = session_factory_from_settings(self.test_app._pyramid_settings)
109 108 return session(self.request)
110 109
111 110
112 111 class TestRequest(webob.BaseRequest):
113 112
114 113 # for py.test, so it doesn't try to run this tas by name starting with test...
115 114 disabled = True
116 115 ResponseClass = CustomTestResponse
117 116
118 117 def add_response_callback(self, callback):
119 118 pass
120 119
121 120 @classmethod
122 121 def blank(cls, path, environ=None, base_url=None,
123 122 headers=None, POST=None, **kw):
124 123
125 124 if not path.isascii():
126 125 # our custom quote path if it contains non-ascii chars
127 126 path = urllib.parse.quote(path)
128 127
129 128 return super(TestRequest, cls).blank(
130 129 path, environ=environ, base_url=base_url, headers=headers, POST=POST, **kw)
131 130
132 131
133 132 class CustomTestApp(TestApp):
134 133 """
135 134 Custom app to make mustcontain more Useful, and extract special methods
136 135 """
137 136 RequestClass = TestRequest
138 137 rc_login_data = {}
139 138 rc_current_session = None
140 139
141 140 def login(self, username=None, password=None):
142 141 from rhodecode.lib import auth
143 142
144 143 if username and password:
145 144 session = login_user_session(self, username, password)
146 145 else:
147 146 session = login_user_session(self)
148 147
149 148 self.rc_login_data['csrf_token'] = auth.get_csrf_token(session)
150 149 self.rc_current_session = session
151 150 return session['rhodecode_user']
152 151
153 152 @property
154 153 def csrf_token(self):
155 154 return self.rc_login_data['csrf_token']
156 155
157 156 @property
158 157 def _pyramid_registry(self):
159 158 return self.app.config.registry
160 159
161 160 @property
162 161 def _pyramid_settings(self):
163 162 return self._pyramid_registry.settings
164 163
165 164 def do_request(self, req, status=None, expect_errors=None):
166 165 # you can put custom code here
167 166 return super().do_request(req, status, expect_errors)
168 167
169 168
170 169 def set_anonymous_access(enabled):
171 170 """(Dis)allows anonymous access depending on parameter `enabled`"""
172 171 user = User.get_default_user()
173 172 user.active = enabled
174 173 Session().add(user)
175 174 Session().commit()
176 175 time.sleep(1.5) # must sleep for cache (1s to expire)
177 176 log.info('anonymous access is now: %s', enabled)
178 177 assert enabled == User.get_default_user().active, (
179 178 'Cannot set anonymous access')
180 179
181 180
182 181 def check_xfail_backends(node, backend_alias):
183 182 # Using "xfail_backends" here intentionally, since this marks work
184 183 # which is "to be done" soon.
185 184 skip_marker = node.get_closest_marker('xfail_backends')
186 185 if skip_marker and backend_alias in skip_marker.args:
187 186 msg = "Support for backend %s to be developed." % (backend_alias, )
188 187 msg = skip_marker.kwargs.get('reason', msg)
189 188 pytest.xfail(msg)
190 189
191 190
192 191 def check_skip_backends(node, backend_alias):
193 192 # Using "skip_backends" here intentionally, since this marks work which is
194 193 # not supported.
195 194 skip_marker = node.get_closest_marker('skip_backends')
196 195 if skip_marker and backend_alias in skip_marker.args:
197 196 msg = "Feature not supported for backend %s." % (backend_alias, )
198 197 msg = skip_marker.kwargs.get('reason', msg)
199 198 pytest.skip(msg)
200 199
201 200
202 201 def extract_git_repo_from_dump(dump_name, repo_name):
203 202 """Create git repo `repo_name` from dump `dump_name`."""
204 203 repos_path = ScmModel().repos_path
205 204 target_path = os.path.join(repos_path, repo_name)
206 205 rc_testdata.extract_git_dump(dump_name, target_path)
207 206 return target_path
208 207
209 208
210 209 def extract_hg_repo_from_dump(dump_name, repo_name):
211 210 """Create hg repo `repo_name` from dump `dump_name`."""
212 211 repos_path = ScmModel().repos_path
213 212 target_path = os.path.join(repos_path, repo_name)
214 213 rc_testdata.extract_hg_dump(dump_name, target_path)
215 214 return target_path
216 215
217 216
218 217 def extract_svn_repo_from_dump(dump_name, repo_name):
219 218 """Create a svn repo `repo_name` from dump `dump_name`."""
220 219 repos_path = ScmModel().repos_path
221 220 target_path = os.path.join(repos_path, repo_name)
222 221 SubversionRepository(target_path, create=True)
223 222 _load_svn_dump_into_repo(dump_name, target_path)
224 223 return target_path
225 224
226 225
227 226 def assert_message_in_log(log_records, message, levelno, module):
228 227 messages = [
229 228 r.message for r in log_records
230 229 if r.module == module and r.levelno == levelno
231 230 ]
232 231 assert message in messages
233 232
234 233
235 234 def _load_svn_dump_into_repo(dump_name, repo_path):
236 235 """
237 236 Utility to populate a svn repository with a named dump
238 237
239 238 Currently the dumps are in rc_testdata. They might later on be
240 239 integrated with the main repository once they stabilize more.
241 240 """
242 241 dump = rc_testdata.load_svn_dump(dump_name)
243 242 load_dump = subprocess.Popen(
244 243 ['svnadmin', 'load', repo_path],
245 244 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
246 245 stderr=subprocess.PIPE)
247 246 out, err = load_dump.communicate(dump)
248 247 if load_dump.returncode != 0:
249 248 log.error("Output of load_dump command: %s", out)
250 249 log.error("Error output of load_dump command: %s", err)
251 250 raise Exception(
252 251 'Failed to load dump "%s" into repository at path "%s".'
253 252 % (dump_name, repo_path))
254 253
255 254
256 255 class AssertResponse(object):
257 256 """
258 257 Utility that helps to assert things about a given HTML response.
259 258 """
260 259
261 260 def __init__(self, response):
262 261 self.response = response
263 262
264 263 def get_imports(self):
265 264 return fromstring, tostring, CSSSelector
266 265
267 266 def one_element_exists(self, css_selector):
268 267 self.get_element(css_selector)
269 268
270 269 def no_element_exists(self, css_selector):
271 270 assert not self._get_elements(css_selector)
272 271
273 272 def element_equals_to(self, css_selector, expected_content):
274 273 element = self.get_element(css_selector)
275 274 element_text = self._element_to_string(element)
276 275
277 276 assert expected_content in element_text
278 277
279 278 def element_contains(self, css_selector, expected_content):
280 279 element = self.get_element(css_selector)
281 280 assert expected_content in element.text_content()
282 281
283 282 def element_value_contains(self, css_selector, expected_content):
284 283 element = self.get_element(css_selector)
285 284 assert expected_content in element.value
286 285
287 286 def contains_one_link(self, link_text, href):
288 287 fromstring, tostring, CSSSelector = self.get_imports()
289 288 doc = fromstring(self.response.body)
290 289 sel = CSSSelector('a[href]')
291 290 elements = [
292 291 e for e in sel(doc) if e.text_content().strip() == link_text]
293 292 assert len(elements) == 1, "Did not find link or found multiple links"
294 293 self._ensure_url_equal(elements[0].attrib.get('href'), href)
295 294
296 295 def contains_one_anchor(self, anchor_id):
297 296 fromstring, tostring, CSSSelector = self.get_imports()
298 297 doc = fromstring(self.response.body)
299 298 sel = CSSSelector('#' + anchor_id)
300 299 elements = sel(doc)
301 300 assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id)
302 301
303 302 def _ensure_url_equal(self, found, expected):
304 303 assert _Url(found) == _Url(expected)
305 304
306 305 def get_element(self, css_selector):
307 306 elements = self._get_elements(css_selector)
308 307 assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector)
309 308 return elements[0]
310 309
311 310 def get_elements(self, css_selector):
312 311 return self._get_elements(css_selector)
313 312
314 313 def _get_elements(self, css_selector):
315 314 fromstring, tostring, CSSSelector = self.get_imports()
316 315 doc = fromstring(self.response.body)
317 316 sel = CSSSelector(css_selector)
318 317 elements = sel(doc)
319 318 return elements
320 319
321 320 def _element_to_string(self, element):
322 321 fromstring, tostring, CSSSelector = self.get_imports()
323 322 return tostring(element, encoding='unicode')
324 323
325 324
326 325 class _Url(object):
327 326 """
328 327 A url object that can be compared with other url orbjects
329 328 without regard to the vagaries of encoding, escaping, and ordering
330 329 of parameters in query strings.
331 330
332 331 Inspired by
333 332 http://stackoverflow.com/questions/5371992/comparing-two-urls-in-python
334 333 """
335 334
336 335 def __init__(self, url):
337 336 parts = urllib.parse.urlparse(url)
338 337 _query = frozenset(urllib.parse.parse_qsl(parts.query))
339 338 _path = unquote_plus(parts.path)
340 339 parts = parts._replace(query=_query, path=_path)
341 340 self.parts = parts
342 341
343 342 def __eq__(self, other):
344 343 return self.parts == other.parts
345 344
346 345 def __hash__(self):
347 346 return hash(self.parts)
348 347
349 348
350 349 def run_test_concurrently(times, raise_catched_exc=True):
351 350 """
352 351 Add this decorator to small pieces of code that you want to test
353 352 concurrently
354 353
355 354 ex:
356 355
357 356 @test_concurrently(25)
358 357 def my_test_function():
359 358 ...
360 359 """
361 360 def test_concurrently_decorator(test_func):
362 361 def wrapper(*args, **kwargs):
363 362 exceptions = []
364 363
365 364 def call_test_func():
366 365 try:
367 366 test_func(*args, **kwargs)
368 367 except Exception as e:
369 368 exceptions.append(e)
370 369 if raise_catched_exc:
371 370 raise
372 371 threads = []
373 372 for i in range(times):
374 373 threads.append(threading.Thread(target=call_test_func))
375 374 for t in threads:
376 375 t.start()
377 376 for t in threads:
378 377 t.join()
379 378 if exceptions:
380 379 raise Exception(
381 380 'test_concurrently intercepted %s exceptions: %s' % (
382 381 len(exceptions), exceptions))
383 382 return wrapper
384 383 return test_concurrently_decorator
385 384
386 385
387 386 def wait_for_url(url, timeout=10):
388 387 """
389 388 Wait until URL becomes reachable.
390 389
391 390 It polls the URL until the timeout is reached or it became reachable.
392 391 If will call to `py.test.fail` in case the URL is not reachable.
393 392 """
394 393 timeout = time.time() + timeout
395 394 last = 0
396 395 wait = 0.1
397 396
398 397 while timeout > last:
399 398 last = time.time()
400 399 if is_url_reachable(url, log_exc=False):
401 400 break
402 401 elif (last + wait) > time.time():
403 402 # Go to sleep because not enough time has passed since last check.
404 403 time.sleep(wait)
405 404 else:
406 405 pytest.fail(f"Timeout while waiting for URL {url}")
407 406
408 407
409 408 def is_url_reachable(url: str, log_exc: bool = False) -> bool:
410 409 try:
411 410 urllib.request.urlopen(url)
412 411 except urllib.error.URLError:
413 412 if log_exc:
414 413 log.exception(f'URL `{url}` reach error')
415 414 return False
416 415 return True
417 416
418 417
419 418 def repo_on_filesystem(repo_name):
420 419 from rhodecode.lib import vcs
421 420 from rhodecode.tests import TESTS_TMP_PATH
422 421 repo = vcs.get_vcs_instance(
423 422 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
424 423 return repo is not None
425 424
426 425
427 426 def commit_change(
428 427 repo, filename: bytes, content: bytes, message, vcs_type, parent=None, newfile=False):
429 428 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
430 429
431 430 repo = Repository.get_by_repo_name(repo)
432 431 _commit = parent
433 432 if not parent:
434 433 _commit = EmptyCommit(alias=vcs_type)
435 434
436 435 if newfile:
437 436 nodes = {
438 437 filename: {
439 438 'content': content
440 439 }
441 440 }
442 441 commit = ScmModel().create_nodes(
443 442 user=TEST_USER_ADMIN_LOGIN, repo=repo,
444 443 message=message,
445 444 nodes=nodes,
446 445 parent_commit=_commit,
447 446 author=f'{TEST_USER_ADMIN_LOGIN} <admin@rhodecode.com>',
448 447 )
449 448 else:
450 449 commit = ScmModel().commit_change(
451 450 repo=repo.scm_instance(), repo_name=repo.repo_name,
452 451 commit=parent, user=TEST_USER_ADMIN_LOGIN,
453 452 author=f'{TEST_USER_ADMIN_LOGIN} <admin@rhodecode.com>',
454 453 message=message,
455 454 content=content,
456 455 f_path=filename
457 456 )
458 457 return commit
459 458
460 459
461 460 def permission_update_data_generator(csrf_token, default=None, grant=None, revoke=None):
462 461 if not default:
463 462 raise ValueError('Permission for default user must be given')
464 463 form_data = [(
465 464 'csrf_token', csrf_token
466 465 )]
467 466 # add default
468 467 form_data.extend([
469 468 ('u_perm_1', default)
470 469 ])
471 470
472 471 if grant:
473 472 for cnt, (obj_id, perm, obj_name, obj_type) in enumerate(grant, 1):
474 473 form_data.extend([
475 474 ('perm_new_member_perm_new{}'.format(cnt), perm),
476 475 ('perm_new_member_id_new{}'.format(cnt), obj_id),
477 476 ('perm_new_member_name_new{}'.format(cnt), obj_name),
478 477 ('perm_new_member_type_new{}'.format(cnt), obj_type),
479 478
480 479 ])
481 480 if revoke:
482 481 for obj_id, obj_type in revoke:
483 482 form_data.extend([
484 483 ('perm_del_member_id_{}'.format(obj_id), obj_id),
485 484 ('perm_del_member_type_{}'.format(obj_id), obj_type),
486 485 ])
487 486 return form_data
General Comments 0
You need to be logged in to leave comments. Login now