##// END OF EJS Templates
db: use more consistent sorting using real objects, strings are deprecated in laters sqlalchemy query syntax.
marcink -
r3949:2da6b4aa default
parent child Browse files
Show More
@@ -1,113 +1,113 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.model.db import UserLog
24 24 from rhodecode.model.pull_request import PullRequestModel
25 25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 26 from rhodecode.api.tests.utils import (
27 27 build_data, api_call, assert_error, assert_ok)
28 28
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestClosePullRequest(object):
32 32
33 33 @pytest.mark.backends("git", "hg")
34 34 def test_api_close_pull_request(self, pr_util):
35 35 pull_request = pr_util.create_pull_request()
36 36 pull_request_id = pull_request.pull_request_id
37 37 author = pull_request.user_id
38 38 repo = pull_request.target_repo.repo_id
39 39 id_, params = build_data(
40 40 self.apikey, 'close_pull_request',
41 41 repoid=pull_request.target_repo.repo_name,
42 42 pullrequestid=pull_request.pull_request_id)
43 43 response = api_call(self.app, params)
44 44 expected = {
45 45 'pull_request_id': pull_request_id,
46 46 'close_status': 'Rejected',
47 47 'closed': True,
48 48 }
49 49 assert_ok(id_, expected, response.body)
50 50 journal = UserLog.query()\
51 51 .filter(UserLog.user_id == author) \
52 .order_by('user_log_id') \
52 .order_by(UserLog.user_log_id.asc()) \
53 53 .filter(UserLog.repository_id == repo)\
54 54 .all()
55 55 assert journal[-1].action == 'repo.pull_request.close'
56 56
57 57 @pytest.mark.backends("git", "hg")
58 58 def test_api_close_pull_request_already_closed_error(self, pr_util):
59 59 pull_request = pr_util.create_pull_request()
60 60 pull_request_id = pull_request.pull_request_id
61 61 pull_request_repo = pull_request.target_repo.repo_name
62 62 PullRequestModel().close_pull_request(
63 63 pull_request, pull_request.author)
64 64 id_, params = build_data(
65 65 self.apikey, 'close_pull_request',
66 66 repoid=pull_request_repo, pullrequestid=pull_request_id)
67 67 response = api_call(self.app, params)
68 68
69 69 expected = 'pull request `%s` is already closed' % pull_request_id
70 70 assert_error(id_, expected, given=response.body)
71 71
72 72 @pytest.mark.backends("git", "hg")
73 73 def test_api_close_pull_request_repo_error(self, pr_util):
74 74 pull_request = pr_util.create_pull_request()
75 75 id_, params = build_data(
76 76 self.apikey, 'close_pull_request',
77 77 repoid=666, pullrequestid=pull_request.pull_request_id)
78 78 response = api_call(self.app, params)
79 79
80 80 expected = 'repository `666` does not exist'
81 81 assert_error(id_, expected, given=response.body)
82 82
83 83 @pytest.mark.backends("git", "hg")
84 84 def test_api_close_pull_request_non_admin_with_userid_error(self,
85 85 pr_util):
86 86 pull_request = pr_util.create_pull_request()
87 87 id_, params = build_data(
88 88 self.apikey_regular, 'close_pull_request',
89 89 repoid=pull_request.target_repo.repo_name,
90 90 pullrequestid=pull_request.pull_request_id,
91 91 userid=TEST_USER_ADMIN_LOGIN)
92 92 response = api_call(self.app, params)
93 93
94 94 expected = 'userid is not the same as your user'
95 95 assert_error(id_, expected, given=response.body)
96 96
97 97 @pytest.mark.backends("git", "hg")
98 98 def test_api_close_pull_request_no_perms_to_close(
99 99 self, user_util, pr_util):
100 100 user = user_util.create_user()
101 101 pull_request = pr_util.create_pull_request()
102 102
103 103 id_, params = build_data(
104 104 user.api_key, 'close_pull_request',
105 105 repoid=pull_request.target_repo.repo_name,
106 106 pullrequestid=pull_request.pull_request_id,)
107 107 response = api_call(self.app, params)
108 108
109 109 expected = ('pull request `%s` close failed, '
110 110 'no permission to close.') % pull_request.pull_request_id
111 111
112 112 response_json = response.json['error']
113 113 assert response_json == expected
@@ -1,209 +1,209 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.model.comment import CommentsModel
24 24 from rhodecode.model.db import UserLog
25 25 from rhodecode.model.pull_request import PullRequestModel
26 26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 27 from rhodecode.api.tests.utils import (
28 28 build_data, api_call, assert_error, assert_ok)
29 29
30 30
31 31 @pytest.mark.usefixtures("testuser_api", "app")
32 32 class TestCommentPullRequest(object):
33 33 finalizers = []
34 34
35 35 def teardown_method(self, method):
36 36 if self.finalizers:
37 37 for finalizer in self.finalizers:
38 38 finalizer()
39 39 self.finalizers = []
40 40
41 41 @pytest.mark.backends("git", "hg")
42 42 def test_api_comment_pull_request(self, pr_util, no_notifications):
43 43 pull_request = pr_util.create_pull_request()
44 44 pull_request_id = pull_request.pull_request_id
45 45 author = pull_request.user_id
46 46 repo = pull_request.target_repo.repo_id
47 47 id_, params = build_data(
48 48 self.apikey, 'comment_pull_request',
49 49 repoid=pull_request.target_repo.repo_name,
50 50 pullrequestid=pull_request.pull_request_id,
51 51 message='test message')
52 52 response = api_call(self.app, params)
53 53 pull_request = PullRequestModel().get(pull_request.pull_request_id)
54 54
55 55 comments = CommentsModel().get_comments(
56 56 pull_request.target_repo.repo_id, pull_request=pull_request)
57 57
58 58 expected = {
59 59 'pull_request_id': pull_request.pull_request_id,
60 60 'comment_id': comments[-1].comment_id,
61 61 'status': {'given': None, 'was_changed': None}
62 62 }
63 63 assert_ok(id_, expected, response.body)
64 64
65 65 journal = UserLog.query()\
66 66 .filter(UserLog.user_id == author)\
67 67 .filter(UserLog.repository_id == repo) \
68 .order_by('user_log_id') \
68 .order_by(UserLog.user_log_id.asc()) \
69 69 .all()
70 70 assert journal[-1].action == 'repo.pull_request.comment.create'
71 71
72 72 @pytest.mark.backends("git", "hg")
73 73 def test_api_comment_pull_request_change_status(
74 74 self, pr_util, no_notifications):
75 75 pull_request = pr_util.create_pull_request()
76 76 pull_request_id = pull_request.pull_request_id
77 77 id_, params = build_data(
78 78 self.apikey, 'comment_pull_request',
79 79 repoid=pull_request.target_repo.repo_name,
80 80 pullrequestid=pull_request.pull_request_id,
81 81 status='rejected')
82 82 response = api_call(self.app, params)
83 83 pull_request = PullRequestModel().get(pull_request_id)
84 84
85 85 comments = CommentsModel().get_comments(
86 86 pull_request.target_repo.repo_id, pull_request=pull_request)
87 87 expected = {
88 88 'pull_request_id': pull_request.pull_request_id,
89 89 'comment_id': comments[-1].comment_id,
90 90 'status': {'given': 'rejected', 'was_changed': True}
91 91 }
92 92 assert_ok(id_, expected, response.body)
93 93
94 94 @pytest.mark.backends("git", "hg")
95 95 def test_api_comment_pull_request_change_status_with_specific_commit_id(
96 96 self, pr_util, no_notifications):
97 97 pull_request = pr_util.create_pull_request()
98 98 pull_request_id = pull_request.pull_request_id
99 99 latest_commit_id = 'test_commit'
100 100 # inject additional revision, to fail test the status change on
101 101 # non-latest commit
102 102 pull_request.revisions = pull_request.revisions + ['test_commit']
103 103
104 104 id_, params = build_data(
105 105 self.apikey, 'comment_pull_request',
106 106 repoid=pull_request.target_repo.repo_name,
107 107 pullrequestid=pull_request.pull_request_id,
108 108 status='approved', commit_id=latest_commit_id)
109 109 response = api_call(self.app, params)
110 110 pull_request = PullRequestModel().get(pull_request_id)
111 111
112 112 expected = {
113 113 'pull_request_id': pull_request.pull_request_id,
114 114 'comment_id': None,
115 115 'status': {'given': 'approved', 'was_changed': False}
116 116 }
117 117 assert_ok(id_, expected, response.body)
118 118
119 119 @pytest.mark.backends("git", "hg")
120 120 def test_api_comment_pull_request_change_status_with_specific_commit_id(
121 121 self, pr_util, no_notifications):
122 122 pull_request = pr_util.create_pull_request()
123 123 pull_request_id = pull_request.pull_request_id
124 124 latest_commit_id = pull_request.revisions[0]
125 125
126 126 id_, params = build_data(
127 127 self.apikey, 'comment_pull_request',
128 128 repoid=pull_request.target_repo.repo_name,
129 129 pullrequestid=pull_request.pull_request_id,
130 130 status='approved', commit_id=latest_commit_id)
131 131 response = api_call(self.app, params)
132 132 pull_request = PullRequestModel().get(pull_request_id)
133 133
134 134 comments = CommentsModel().get_comments(
135 135 pull_request.target_repo.repo_id, pull_request=pull_request)
136 136 expected = {
137 137 'pull_request_id': pull_request.pull_request_id,
138 138 'comment_id': comments[-1].comment_id,
139 139 'status': {'given': 'approved', 'was_changed': True}
140 140 }
141 141 assert_ok(id_, expected, response.body)
142 142
143 143 @pytest.mark.backends("git", "hg")
144 144 def test_api_comment_pull_request_missing_params_error(self, pr_util):
145 145 pull_request = pr_util.create_pull_request()
146 146 pull_request_id = pull_request.pull_request_id
147 147 pull_request_repo = pull_request.target_repo.repo_name
148 148 id_, params = build_data(
149 149 self.apikey, 'comment_pull_request',
150 150 repoid=pull_request_repo,
151 151 pullrequestid=pull_request_id)
152 152 response = api_call(self.app, params)
153 153
154 154 expected = 'Both message and status parameters are missing. At least one is required.'
155 155 assert_error(id_, expected, given=response.body)
156 156
157 157 @pytest.mark.backends("git", "hg")
158 158 def test_api_comment_pull_request_unknown_status_error(self, pr_util):
159 159 pull_request = pr_util.create_pull_request()
160 160 pull_request_id = pull_request.pull_request_id
161 161 pull_request_repo = pull_request.target_repo.repo_name
162 162 id_, params = build_data(
163 163 self.apikey, 'comment_pull_request',
164 164 repoid=pull_request_repo,
165 165 pullrequestid=pull_request_id,
166 166 status='42')
167 167 response = api_call(self.app, params)
168 168
169 169 expected = 'Unknown comment status: `42`'
170 170 assert_error(id_, expected, given=response.body)
171 171
172 172 @pytest.mark.backends("git", "hg")
173 173 def test_api_comment_pull_request_repo_error(self, pr_util):
174 174 pull_request = pr_util.create_pull_request()
175 175 id_, params = build_data(
176 176 self.apikey, 'comment_pull_request',
177 177 repoid=666, pullrequestid=pull_request.pull_request_id)
178 178 response = api_call(self.app, params)
179 179
180 180 expected = 'repository `666` does not exist'
181 181 assert_error(id_, expected, given=response.body)
182 182
183 183 @pytest.mark.backends("git", "hg")
184 184 def test_api_comment_pull_request_non_admin_with_userid_error(
185 185 self, pr_util):
186 186 pull_request = pr_util.create_pull_request()
187 187 id_, params = build_data(
188 188 self.apikey_regular, 'comment_pull_request',
189 189 repoid=pull_request.target_repo.repo_name,
190 190 pullrequestid=pull_request.pull_request_id,
191 191 userid=TEST_USER_ADMIN_LOGIN)
192 192 response = api_call(self.app, params)
193 193
194 194 expected = 'userid is not the same as your user'
195 195 assert_error(id_, expected, given=response.body)
196 196
197 197 @pytest.mark.backends("git", "hg")
198 198 def test_api_comment_pull_request_wrong_commit_id_error(self, pr_util):
199 199 pull_request = pr_util.create_pull_request()
200 200 id_, params = build_data(
201 201 self.apikey_regular, 'comment_pull_request',
202 202 repoid=pull_request.target_repo.repo_name,
203 203 status='approved',
204 204 pullrequestid=pull_request.pull_request_id,
205 205 commit_id='XXX')
206 206 response = api_call(self.app, params)
207 207
208 208 expected = 'Invalid commit_id `XXX` for this pull request.'
209 209 assert_error(id_, expected, given=response.body)
@@ -1,259 +1,259 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.model.db import UserLog, PullRequest
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 26 from rhodecode.api.tests.utils import (
27 27 build_data, api_call, assert_error, assert_ok)
28 28
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestMergePullRequest(object):
32 32
33 33 @pytest.mark.backends("git", "hg")
34 34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
35 35 pull_request = pr_util.create_pull_request(mergeable=True)
36 36 pull_request_id = pull_request.pull_request_id
37 37 pull_request_repo = pull_request.target_repo.repo_name
38 38
39 39 id_, params = build_data(
40 40 self.apikey, 'merge_pull_request',
41 41 repoid=pull_request_repo,
42 42 pullrequestid=pull_request_id)
43 43
44 44 response = api_call(self.app, params)
45 45
46 46 # The above api call detaches the pull request DB object from the
47 47 # session because of an unconditional transaction rollback in our
48 48 # middleware. Therefore we need to add it back here if we want to use it.
49 49 Session().add(pull_request)
50 50
51 51 expected = 'merge not possible for following reasons: ' \
52 52 'Pull request reviewer approval is pending.'
53 53 assert_error(id_, expected, given=response.body)
54 54
55 55 @pytest.mark.backends("git", "hg")
56 56 def test_api_merge_pull_request_merge_failed_disallowed_state(
57 57 self, pr_util, no_notifications):
58 58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
59 59 pull_request_id = pull_request.pull_request_id
60 60 pull_request_repo = pull_request.target_repo.repo_name
61 61
62 62 pr = PullRequest.get(pull_request_id)
63 63 pr.pull_request_state = pull_request.STATE_UPDATING
64 64 Session().add(pr)
65 65 Session().commit()
66 66
67 67 id_, params = build_data(
68 68 self.apikey, 'merge_pull_request',
69 69 repoid=pull_request_repo,
70 70 pullrequestid=pull_request_id)
71 71
72 72 response = api_call(self.app, params)
73 73 expected = 'Operation forbidden because pull request is in state {}, '\
74 74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
75 75 PullRequest.STATE_CREATED)
76 76 assert_error(id_, expected, given=response.body)
77 77
78 78 @pytest.mark.backends("git", "hg")
79 79 def test_api_merge_pull_request(self, pr_util, no_notifications):
80 80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
81 81 author = pull_request.user_id
82 82 repo = pull_request.target_repo.repo_id
83 83 pull_request_id = pull_request.pull_request_id
84 84 pull_request_repo = pull_request.target_repo.repo_name
85 85
86 86 id_, params = build_data(
87 87 self.apikey, 'comment_pull_request',
88 88 repoid=pull_request_repo,
89 89 pullrequestid=pull_request_id,
90 90 status='approved')
91 91
92 92 response = api_call(self.app, params)
93 93 expected = {
94 94 'comment_id': response.json.get('result', {}).get('comment_id'),
95 95 'pull_request_id': pull_request_id,
96 96 'status': {'given': 'approved', 'was_changed': True}
97 97 }
98 98 assert_ok(id_, expected, given=response.body)
99 99
100 100 id_, params = build_data(
101 101 self.apikey, 'merge_pull_request',
102 102 repoid=pull_request_repo,
103 103 pullrequestid=pull_request_id)
104 104
105 105 response = api_call(self.app, params)
106 106
107 107 pull_request = PullRequest.get(pull_request_id)
108 108
109 109 expected = {
110 110 'executed': True,
111 111 'failure_reason': 0,
112 112 'merge_status_message': 'This pull request can be automatically merged.',
113 113 'possible': True,
114 114 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
115 115 'merge_ref': pull_request.shadow_merge_ref._asdict()
116 116 }
117 117
118 118 assert_ok(id_, expected, response.body)
119 119
120 120 journal = UserLog.query()\
121 121 .filter(UserLog.user_id == author)\
122 122 .filter(UserLog.repository_id == repo) \
123 .order_by('user_log_id') \
123 .order_by(UserLog.user_log_id.asc()) \
124 124 .all()
125 125 assert journal[-2].action == 'repo.pull_request.merge'
126 126 assert journal[-1].action == 'repo.pull_request.close'
127 127
128 128 id_, params = build_data(
129 129 self.apikey, 'merge_pull_request',
130 130 repoid=pull_request_repo, pullrequestid=pull_request_id)
131 131 response = api_call(self.app, params)
132 132
133 133 expected = 'merge not possible for following reasons: This pull request is closed.'
134 134 assert_error(id_, expected, given=response.body)
135 135
136 136 @pytest.mark.backends("git", "hg")
137 137 def test_api_merge_pull_request_as_another_user_no_perms_to_merge(
138 138 self, pr_util, no_notifications, user_util):
139 139 merge_user = user_util.create_user()
140 140 merge_user_id = merge_user.user_id
141 141 merge_user_username = merge_user.username
142 142
143 143 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
144 144
145 145 pull_request_id = pull_request.pull_request_id
146 146 pull_request_repo = pull_request.target_repo.repo_name
147 147
148 148 id_, params = build_data(
149 149 self.apikey, 'comment_pull_request',
150 150 repoid=pull_request_repo,
151 151 pullrequestid=pull_request_id,
152 152 status='approved')
153 153
154 154 response = api_call(self.app, params)
155 155 expected = {
156 156 'comment_id': response.json.get('result', {}).get('comment_id'),
157 157 'pull_request_id': pull_request_id,
158 158 'status': {'given': 'approved', 'was_changed': True}
159 159 }
160 160 assert_ok(id_, expected, given=response.body)
161 161 id_, params = build_data(
162 162 self.apikey, 'merge_pull_request',
163 163 repoid=pull_request_repo,
164 164 pullrequestid=pull_request_id,
165 165 userid=merge_user_id
166 166 )
167 167
168 168 response = api_call(self.app, params)
169 169 expected = 'merge not possible for following reasons: User `{}` ' \
170 170 'not allowed to perform merge.'.format(merge_user_username)
171 171 assert_error(id_, expected, response.body)
172 172
173 173 @pytest.mark.backends("git", "hg")
174 174 def test_api_merge_pull_request_as_another_user(self, pr_util, no_notifications, user_util):
175 175 merge_user = user_util.create_user()
176 176 merge_user_id = merge_user.user_id
177 177 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
178 178 user_util.grant_user_permission_to_repo(
179 179 pull_request.target_repo, merge_user, 'repository.write')
180 180 author = pull_request.user_id
181 181 repo = pull_request.target_repo.repo_id
182 182 pull_request_id = pull_request.pull_request_id
183 183 pull_request_repo = pull_request.target_repo.repo_name
184 184
185 185 id_, params = build_data(
186 186 self.apikey, 'comment_pull_request',
187 187 repoid=pull_request_repo,
188 188 pullrequestid=pull_request_id,
189 189 status='approved')
190 190
191 191 response = api_call(self.app, params)
192 192 expected = {
193 193 'comment_id': response.json.get('result', {}).get('comment_id'),
194 194 'pull_request_id': pull_request_id,
195 195 'status': {'given': 'approved', 'was_changed': True}
196 196 }
197 197 assert_ok(id_, expected, given=response.body)
198 198
199 199 id_, params = build_data(
200 200 self.apikey, 'merge_pull_request',
201 201 repoid=pull_request_repo,
202 202 pullrequestid=pull_request_id,
203 203 userid=merge_user_id
204 204 )
205 205
206 206 response = api_call(self.app, params)
207 207
208 208 pull_request = PullRequest.get(pull_request_id)
209 209
210 210 expected = {
211 211 'executed': True,
212 212 'failure_reason': 0,
213 213 'merge_status_message': 'This pull request can be automatically merged.',
214 214 'possible': True,
215 215 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
216 216 'merge_ref': pull_request.shadow_merge_ref._asdict()
217 217 }
218 218
219 219 assert_ok(id_, expected, response.body)
220 220
221 221 journal = UserLog.query() \
222 222 .filter(UserLog.user_id == merge_user_id) \
223 223 .filter(UserLog.repository_id == repo) \
224 .order_by('user_log_id') \
224 .order_by(UserLog.user_log_id.asc()) \
225 225 .all()
226 226 assert journal[-2].action == 'repo.pull_request.merge'
227 227 assert journal[-1].action == 'repo.pull_request.close'
228 228
229 229 id_, params = build_data(
230 230 self.apikey, 'merge_pull_request',
231 231 repoid=pull_request_repo, pullrequestid=pull_request_id, userid=merge_user_id)
232 232 response = api_call(self.app, params)
233 233
234 234 expected = 'merge not possible for following reasons: This pull request is closed.'
235 235 assert_error(id_, expected, given=response.body)
236 236
237 237 @pytest.mark.backends("git", "hg")
238 238 def test_api_merge_pull_request_repo_error(self, pr_util):
239 239 pull_request = pr_util.create_pull_request()
240 240 id_, params = build_data(
241 241 self.apikey, 'merge_pull_request',
242 242 repoid=666, pullrequestid=pull_request.pull_request_id)
243 243 response = api_call(self.app, params)
244 244
245 245 expected = 'repository `666` does not exist'
246 246 assert_error(id_, expected, given=response.body)
247 247
248 248 @pytest.mark.backends("git", "hg")
249 249 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
250 250 pull_request = pr_util.create_pull_request(mergeable=True)
251 251 id_, params = build_data(
252 252 self.apikey_regular, 'merge_pull_request',
253 253 repoid=pull_request.target_repo.repo_name,
254 254 pullrequestid=pull_request.pull_request_id,
255 255 userid=TEST_USER_ADMIN_LOGIN)
256 256 response = api_call(self.app, params)
257 257
258 258 expected = 'userid is not the same as your user'
259 259 assert_error(id_, expected, given=response.body)
@@ -1,362 +1,362 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import datetime
21 21 import logging
22 22 import formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, CSRFRequired, NotAnonymous,
35 35 HasPermissionAny, HasRepoGroupPermissionAny)
36 36 from rhodecode.lib import helpers as h, audit_logger
37 37 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
38 38 from rhodecode.model.forms import RepoGroupForm
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.repo_group import RepoGroupModel
41 41 from rhodecode.model.scm import RepoGroupList
42 42 from rhodecode.model.db import (
43 43 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
49 49
50 50 def load_default_context(self):
51 51 c = self._get_local_tmpl_context()
52 52
53 53 return c
54 54
55 55 def _load_form_data(self, c):
56 56 allow_empty_group = False
57 57
58 58 if self._can_create_repo_group():
59 59 # we're global admin, we're ok and we can create TOP level groups
60 60 allow_empty_group = True
61 61
62 62 # override the choices for this form, we need to filter choices
63 63 # and display only those we have ADMIN right
64 64 groups_with_admin_rights = RepoGroupList(
65 65 RepoGroup.query().all(),
66 66 perm_set=['group.admin'])
67 67 c.repo_groups = RepoGroup.groups_choices(
68 68 groups=groups_with_admin_rights,
69 69 show_empty_group=allow_empty_group)
70 70
71 71 def _can_create_repo_group(self, parent_group_id=None):
72 72 is_admin = HasPermissionAny('hg.admin')('group create controller')
73 73 create_repo_group = HasPermissionAny(
74 74 'hg.repogroup.create.true')('group create controller')
75 75 if is_admin or (create_repo_group and not parent_group_id):
76 76 # we're global admin, or we have global repo group create
77 77 # permission
78 78 # we're ok and we can create TOP level groups
79 79 return True
80 80 elif parent_group_id:
81 81 # we check the permission if we can write to parent group
82 82 group = RepoGroup.get(parent_group_id)
83 83 group_name = group.group_name if group else None
84 84 if HasRepoGroupPermissionAny('group.admin')(
85 85 group_name, 'check if user is an admin of group'):
86 86 # we're an admin of passed in group, we're ok.
87 87 return True
88 88 else:
89 89 return False
90 90 return False
91 91
92 92 # permission check in data loading of
93 93 # `repo_group_list_data` via RepoGroupList
94 94 @LoginRequired()
95 95 @NotAnonymous()
96 96 @view_config(
97 97 route_name='repo_groups', request_method='GET',
98 98 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
99 99 def repo_group_list(self):
100 100 c = self.load_default_context()
101 101 return self._get_template_context(c)
102 102
103 103 # permission check inside
104 104 @LoginRequired()
105 105 @NotAnonymous()
106 106 @view_config(
107 107 route_name='repo_groups_data', request_method='GET',
108 108 renderer='json_ext', xhr=True)
109 109 def repo_group_list_data(self):
110 110 self.load_default_context()
111 111 column_map = {
112 112 'name_raw': 'group_name_hash',
113 113 'desc': 'group_description',
114 114 'last_change_raw': 'updated_on',
115 115 'top_level_repos': 'repos_total',
116 116 'owner': 'user_username',
117 117 }
118 118 draw, start, limit = self._extract_chunk(self.request)
119 119 search_q, order_by, order_dir = self._extract_ordering(
120 120 self.request, column_map=column_map)
121 121
122 122 _render = self.request.get_partial_renderer(
123 123 'rhodecode:templates/data_table/_dt_elements.mako')
124 124 c = _render.get_call_context()
125 125
126 126 def quick_menu(repo_group_name):
127 127 return _render('quick_repo_group_menu', repo_group_name)
128 128
129 129 def repo_group_lnk(repo_group_name):
130 130 return _render('repo_group_name', repo_group_name)
131 131
132 132 def last_change(last_change):
133 133 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
134 134 delta = datetime.timedelta(
135 135 seconds=(datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
136 136 last_change = last_change + delta
137 137 return _render("last_change", last_change)
138 138
139 139 def desc(desc, personal):
140 140 return _render(
141 141 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
142 142
143 143 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
144 144 return _render(
145 145 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
146 146
147 147 def user_profile(username):
148 148 return _render('user_profile', username)
149 149
150 150 auth_repo_group_list = RepoGroupList(
151 151 RepoGroup.query().all(), perm_set=['group.admin'])
152 152
153 153 allowed_ids = [-1]
154 154 for repo_group in auth_repo_group_list:
155 155 allowed_ids.append(repo_group.group_id)
156 156
157 157 repo_groups_data_total_count = RepoGroup.query()\
158 158 .filter(or_(
159 159 # generate multiple IN to fix limitation problems
160 160 *in_filter_generator(RepoGroup.group_id, allowed_ids)
161 161 )) \
162 162 .count()
163 163
164 164 repo_groups_data_total_inactive_count = RepoGroup.query()\
165 165 .filter(RepoGroup.group_id.in_(allowed_ids))\
166 166 .count()
167 167
168 168 repo_count = count(Repository.repo_id)
169 169 base_q = Session.query(
170 170 RepoGroup.group_name,
171 171 RepoGroup.group_name_hash,
172 172 RepoGroup.group_description,
173 173 RepoGroup.group_id,
174 174 RepoGroup.personal,
175 175 RepoGroup.updated_on,
176 176 User,
177 177 repo_count.label('repos_count')
178 178 ) \
179 179 .filter(or_(
180 180 # generate multiple IN to fix limitation problems
181 181 *in_filter_generator(RepoGroup.group_id, allowed_ids)
182 182 )) \
183 .outerjoin(Repository) \
183 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
184 184 .join(User, User.user_id == RepoGroup.user_id) \
185 185 .group_by(RepoGroup, User)
186 186
187 187 if search_q:
188 188 like_expression = u'%{}%'.format(safe_unicode(search_q))
189 189 base_q = base_q.filter(or_(
190 190 RepoGroup.group_name.ilike(like_expression),
191 191 ))
192 192
193 193 repo_groups_data_total_filtered_count = base_q.count()
194 194 # the inactive isn't really used, but we still make it same as other data grids
195 195 # which use inactive (users,user groups)
196 196 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
197 197
198 198 sort_defined = False
199 199 if order_by == 'group_name':
200 200 sort_col = func.lower(RepoGroup.group_name)
201 201 sort_defined = True
202 202 elif order_by == 'repos_total':
203 203 sort_col = repo_count
204 204 sort_defined = True
205 205 elif order_by == 'user_username':
206 206 sort_col = User.username
207 207 else:
208 208 sort_col = getattr(RepoGroup, order_by, None)
209 209
210 210 if sort_defined or sort_col:
211 211 if order_dir == 'asc':
212 212 sort_col = sort_col.asc()
213 213 else:
214 214 sort_col = sort_col.desc()
215 215
216 216 base_q = base_q.order_by(sort_col)
217 217 base_q = base_q.offset(start).limit(limit)
218 218
219 219 # authenticated access to user groups
220 220 auth_repo_group_list = base_q.all()
221 221
222 222 repo_groups_data = []
223 223 for repo_gr in auth_repo_group_list:
224 224 row = {
225 225 "menu": quick_menu(repo_gr.group_name),
226 226 "name": repo_group_lnk(repo_gr.group_name),
227 227 "name_raw": repo_gr.group_name,
228 228 "last_change": last_change(repo_gr.updated_on),
229 229 "last_change_raw": datetime_to_time(repo_gr.updated_on),
230 230
231 231 "last_changeset": "",
232 232 "last_changeset_raw": "",
233 233
234 234 "desc": desc(repo_gr.group_description, repo_gr.personal),
235 235 "owner": user_profile(repo_gr.User.username),
236 236 "top_level_repos": repo_gr.repos_count,
237 237 "action": repo_group_actions(
238 238 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
239 239
240 240 }
241 241
242 242 repo_groups_data.append(row)
243 243
244 244 data = ({
245 245 'draw': draw,
246 246 'data': repo_groups_data,
247 247 'recordsTotal': repo_groups_data_total_count,
248 248 'recordsTotalInactive': repo_groups_data_total_inactive_count,
249 249 'recordsFiltered': repo_groups_data_total_filtered_count,
250 250 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
251 251 })
252 252
253 253 return data
254 254
255 255 @LoginRequired()
256 256 @NotAnonymous()
257 257 # perm checks inside
258 258 @view_config(
259 259 route_name='repo_group_new', request_method='GET',
260 260 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
261 261 def repo_group_new(self):
262 262 c = self.load_default_context()
263 263
264 264 # perm check for admin, create_group perm or admin of parent_group
265 265 parent_group_id = safe_int(self.request.GET.get('parent_group'))
266 266 if not self._can_create_repo_group(parent_group_id):
267 267 raise HTTPForbidden()
268 268
269 269 self._load_form_data(c)
270 270
271 271 defaults = {} # Future proof for default of repo group
272 272 data = render(
273 273 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
274 274 self._get_template_context(c), self.request)
275 275 html = formencode.htmlfill.render(
276 276 data,
277 277 defaults=defaults,
278 278 encoding="UTF-8",
279 279 force_defaults=False
280 280 )
281 281 return Response(html)
282 282
283 283 @LoginRequired()
284 284 @NotAnonymous()
285 285 @CSRFRequired()
286 286 # perm checks inside
287 287 @view_config(
288 288 route_name='repo_group_create', request_method='POST',
289 289 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
290 290 def repo_group_create(self):
291 291 c = self.load_default_context()
292 292 _ = self.request.translate
293 293
294 294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
295 295 can_create = self._can_create_repo_group(parent_group_id)
296 296
297 297 self._load_form_data(c)
298 298 # permissions for can create group based on parent_id are checked
299 299 # here in the Form
300 300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
301 301 repo_group_form = RepoGroupForm(
302 302 self.request.translate, available_groups=available_groups,
303 303 can_create_in_root=can_create)()
304 304
305 305 repo_group_name = self.request.POST.get('group_name')
306 306 try:
307 307 owner = self._rhodecode_user
308 308 form_result = repo_group_form.to_python(dict(self.request.POST))
309 309 copy_permissions = form_result.get('group_copy_permissions')
310 310 repo_group = RepoGroupModel().create(
311 311 group_name=form_result['group_name_full'],
312 312 group_description=form_result['group_description'],
313 313 owner=owner.user_id,
314 314 copy_permissions=form_result['group_copy_permissions']
315 315 )
316 316 Session().flush()
317 317
318 318 repo_group_data = repo_group.get_api_data()
319 319 audit_logger.store_web(
320 320 'repo_group.create', action_data={'data': repo_group_data},
321 321 user=self._rhodecode_user)
322 322
323 323 Session().commit()
324 324
325 325 _new_group_name = form_result['group_name_full']
326 326
327 327 repo_group_url = h.link_to(
328 328 _new_group_name,
329 329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
330 330 h.flash(h.literal(_('Created repository group %s')
331 331 % repo_group_url), category='success')
332 332
333 333 except formencode.Invalid as errors:
334 334 data = render(
335 335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
336 336 self._get_template_context(c), self.request)
337 337 html = formencode.htmlfill.render(
338 338 data,
339 339 defaults=errors.value,
340 340 errors=errors.error_dict or {},
341 341 prefix_error=False,
342 342 encoding="UTF-8",
343 343 force_defaults=False
344 344 )
345 345 return Response(html)
346 346 except Exception:
347 347 log.exception("Exception during creation of repository group")
348 348 h.flash(_('Error occurred during creation of repository group %s')
349 349 % repo_group_name, category='error')
350 350 raise HTTPFound(h.route_path('home'))
351 351
352 352 affected_user_ids = [self._rhodecode_user.user_id]
353 353 if copy_permissions:
354 354 user_group_perms = repo_group.permissions(expand_from_user_groups=True)
355 355 copy_perms = [perm['user_id'] for perm in user_group_perms]
356 356 # also include those newly created by copy
357 357 affected_user_ids.extend(copy_perms)
358 358 PermissionModel().trigger_permission_flush(affected_user_ids)
359 359
360 360 raise HTTPFound(
361 361 h.route_path('repo_group_home',
362 362 repo_group_name=form_result['group_name_full']))
@@ -1,273 +1,273 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
35 35 from rhodecode.lib import helpers as h, audit_logger
36 36 from rhodecode.lib.utils2 import safe_unicode
37 37
38 38 from rhodecode.model.forms import UserGroupForm
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.scm import UserGroupList
41 41 from rhodecode.model.db import (
42 42 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.user_group import UserGroupModel
45 45 from rhodecode.model.db import true
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminUserGroupsView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54
55 55 PermissionModel().set_global_permission_choices(
56 56 c, gettext_translator=self.request.translate)
57 57
58 58 return c
59 59
60 60 # permission check in data loading of
61 61 # `user_groups_list_data` via UserGroupList
62 62 @LoginRequired()
63 63 @NotAnonymous()
64 64 @view_config(
65 65 route_name='user_groups', request_method='GET',
66 66 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
67 67 def user_groups_list(self):
68 68 c = self.load_default_context()
69 69 return self._get_template_context(c)
70 70
71 71 # permission check inside
72 72 @LoginRequired()
73 73 @NotAnonymous()
74 74 @view_config(
75 75 route_name='user_groups_data', request_method='GET',
76 76 renderer='json_ext', xhr=True)
77 77 def user_groups_list_data(self):
78 78 self.load_default_context()
79 79 column_map = {
80 80 'active': 'users_group_active',
81 81 'description': 'user_group_description',
82 82 'members': 'members_total',
83 83 'owner': 'user_username',
84 84 'sync': 'group_data'
85 85 }
86 86 draw, start, limit = self._extract_chunk(self.request)
87 87 search_q, order_by, order_dir = self._extract_ordering(
88 88 self.request, column_map=column_map)
89 89
90 90 _render = self.request.get_partial_renderer(
91 91 'rhodecode:templates/data_table/_dt_elements.mako')
92 92
93 93 def user_group_name(user_group_name):
94 94 return _render("user_group_name", user_group_name)
95 95
96 96 def user_group_actions(user_group_id, user_group_name):
97 97 return _render("user_group_actions", user_group_id, user_group_name)
98 98
99 99 def user_profile(username):
100 100 return _render('user_profile', username)
101 101
102 102 auth_user_group_list = UserGroupList(
103 103 UserGroup.query().all(), perm_set=['usergroup.admin'])
104 104
105 105 allowed_ids = [-1]
106 106 for user_group in auth_user_group_list:
107 107 allowed_ids.append(user_group.users_group_id)
108 108
109 109 user_groups_data_total_count = UserGroup.query()\
110 110 .filter(or_(
111 111 # generate multiple IN to fix limitation problems
112 112 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
113 113 ))\
114 114 .count()
115 115
116 116 user_groups_data_total_inactive_count = UserGroup.query()\
117 117 .filter(or_(
118 118 # generate multiple IN to fix limitation problems
119 119 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
120 120 ))\
121 121 .filter(UserGroup.users_group_active != true()).count()
122 122
123 123 member_count = count(UserGroupMember.user_id)
124 124 base_q = Session.query(
125 125 UserGroup.users_group_name,
126 126 UserGroup.user_group_description,
127 127 UserGroup.users_group_active,
128 128 UserGroup.users_group_id,
129 129 UserGroup.group_data,
130 130 User,
131 131 member_count.label('member_count')
132 132 ) \
133 133 .filter(or_(
134 134 # generate multiple IN to fix limitation problems
135 135 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
136 136 )) \
137 .outerjoin(UserGroupMember) \
137 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
138 138 .join(User, User.user_id == UserGroup.user_id) \
139 139 .group_by(UserGroup, User)
140 140
141 141 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
142 142
143 143 if search_q:
144 144 like_expression = u'%{}%'.format(safe_unicode(search_q))
145 145 base_q = base_q.filter(or_(
146 146 UserGroup.users_group_name.ilike(like_expression),
147 147 ))
148 148 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
149 149
150 150 user_groups_data_total_filtered_count = base_q.count()
151 151 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
152 152
153 153 sort_defined = False
154 154 if order_by == 'members_total':
155 155 sort_col = member_count
156 156 sort_defined = True
157 157 elif order_by == 'user_username':
158 158 sort_col = User.username
159 159 else:
160 160 sort_col = getattr(UserGroup, order_by, None)
161 161
162 162 if sort_defined or sort_col:
163 163 if order_dir == 'asc':
164 164 sort_col = sort_col.asc()
165 165 else:
166 166 sort_col = sort_col.desc()
167 167
168 168 base_q = base_q.order_by(sort_col)
169 169 base_q = base_q.offset(start).limit(limit)
170 170
171 171 # authenticated access to user groups
172 172 auth_user_group_list = base_q.all()
173 173
174 174 user_groups_data = []
175 175 for user_gr in auth_user_group_list:
176 176 row = {
177 177 "users_group_name": user_group_name(user_gr.users_group_name),
178 178 "name_raw": h.escape(user_gr.users_group_name),
179 179 "description": h.escape(user_gr.user_group_description),
180 180 "members": user_gr.member_count,
181 181 # NOTE(marcink): because of advanced query we
182 182 # need to load it like that
183 183 "sync": UserGroup._load_sync(
184 184 UserGroup._load_group_data(user_gr.group_data)),
185 185 "active": h.bool2icon(user_gr.users_group_active),
186 186 "owner": user_profile(user_gr.User.username),
187 187 "action": user_group_actions(
188 188 user_gr.users_group_id, user_gr.users_group_name)
189 189 }
190 190 user_groups_data.append(row)
191 191
192 192 data = ({
193 193 'draw': draw,
194 194 'data': user_groups_data,
195 195 'recordsTotal': user_groups_data_total_count,
196 196 'recordsTotalInactive': user_groups_data_total_inactive_count,
197 197 'recordsFiltered': user_groups_data_total_filtered_count,
198 198 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
199 199 })
200 200
201 201 return data
202 202
203 203 @LoginRequired()
204 204 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
205 205 @view_config(
206 206 route_name='user_groups_new', request_method='GET',
207 207 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
208 208 def user_groups_new(self):
209 209 c = self.load_default_context()
210 210 return self._get_template_context(c)
211 211
212 212 @LoginRequired()
213 213 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
214 214 @CSRFRequired()
215 215 @view_config(
216 216 route_name='user_groups_create', request_method='POST',
217 217 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
218 218 def user_groups_create(self):
219 219 _ = self.request.translate
220 220 c = self.load_default_context()
221 221 users_group_form = UserGroupForm(self.request.translate)()
222 222
223 223 user_group_name = self.request.POST.get('users_group_name')
224 224 try:
225 225 form_result = users_group_form.to_python(dict(self.request.POST))
226 226 user_group = UserGroupModel().create(
227 227 name=form_result['users_group_name'],
228 228 description=form_result['user_group_description'],
229 229 owner=self._rhodecode_user.user_id,
230 230 active=form_result['users_group_active'])
231 231 Session().flush()
232 232 creation_data = user_group.get_api_data()
233 233 user_group_name = form_result['users_group_name']
234 234
235 235 audit_logger.store_web(
236 236 'user_group.create', action_data={'data': creation_data},
237 237 user=self._rhodecode_user)
238 238
239 239 user_group_link = h.link_to(
240 240 h.escape(user_group_name),
241 241 h.route_path(
242 242 'edit_user_group', user_group_id=user_group.users_group_id))
243 243 h.flash(h.literal(_('Created user group %(user_group_link)s')
244 244 % {'user_group_link': user_group_link}),
245 245 category='success')
246 246 Session().commit()
247 247 user_group_id = user_group.users_group_id
248 248 except formencode.Invalid as errors:
249 249
250 250 data = render(
251 251 'rhodecode:templates/admin/user_groups/user_group_add.mako',
252 252 self._get_template_context(c), self.request)
253 253 html = formencode.htmlfill.render(
254 254 data,
255 255 defaults=errors.value,
256 256 errors=errors.error_dict or {},
257 257 prefix_error=False,
258 258 encoding="UTF-8",
259 259 force_defaults=False
260 260 )
261 261 return Response(html)
262 262
263 263 except Exception:
264 264 log.exception("Exception creating user group")
265 265 h.flash(_('Error occurred during creation of user group %s') \
266 266 % user_group_name, category='error')
267 267 raise HTTPFound(h.route_path('user_groups_new'))
268 268
269 269 affected_user_ids = [self._rhodecode_user.user_id]
270 270 PermissionModel().trigger_permission_flush(affected_user_ids)
271 271
272 272 raise HTTPFound(
273 273 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1221 +1,1221 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib import helpers as h
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.tests import (
34 34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39
40 40 base_url = {
41 41 'repo_changelog': '/{repo_name}/changelog',
42 42 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 43 'repo_commits': '/{repo_name}/commits',
44 44 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 45 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 46 'pullrequest_show_all': '/{repo_name}/pull-request',
47 47 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 48 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 49 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 50 'pullrequest_new': '/{repo_name}/pull-request/new',
51 51 'pullrequest_create': '/{repo_name}/pull-request/create',
52 52 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 53 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 54 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 55 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 56 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 57 }[name].format(**kwargs)
58 58
59 59 if params:
60 60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
61 61 return base_url
62 62
63 63
64 64 @pytest.mark.usefixtures('app', 'autologin_user')
65 65 @pytest.mark.backends("git", "hg")
66 66 class TestPullrequestsView(object):
67 67
68 68 def test_index(self, backend):
69 69 self.app.get(route_path(
70 70 'pullrequest_new',
71 71 repo_name=backend.repo_name))
72 72
73 73 def test_option_menu_create_pull_request_exists(self, backend):
74 74 repo_name = backend.repo_name
75 75 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
76 76
77 77 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
78 78 'pullrequest_new', repo_name=repo_name)
79 79 response.mustcontain(create_pr_link)
80 80
81 81 def test_create_pr_form_with_raw_commit_id(self, backend):
82 82 repo = backend.repo
83 83
84 84 self.app.get(
85 85 route_path('pullrequest_new', repo_name=repo.repo_name,
86 86 commit=repo.get_commit().raw_id),
87 87 status=200)
88 88
89 89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 90 @pytest.mark.parametrize('range_diff', ["0", "1"])
91 91 def test_show(self, pr_util, pr_merge_enabled, range_diff):
92 92 pull_request = pr_util.create_pull_request(
93 93 mergeable=pr_merge_enabled, enable_notifications=False)
94 94
95 95 response = self.app.get(route_path(
96 96 'pullrequest_show',
97 97 repo_name=pull_request.target_repo.scm_instance().name,
98 98 pull_request_id=pull_request.pull_request_id,
99 99 params={'range-diff': range_diff}))
100 100
101 101 for commit_id in pull_request.revisions:
102 102 response.mustcontain(commit_id)
103 103
104 104 assert pull_request.target_ref_parts.type in response
105 105 assert pull_request.target_ref_parts.name in response
106 106 target_clone_url = pull_request.target_repo.clone_url()
107 107 assert target_clone_url in response
108 108
109 109 assert 'class="pull-request-merge"' in response
110 110 if pr_merge_enabled:
111 111 response.mustcontain('Pull request reviewer approval is pending')
112 112 else:
113 113 response.mustcontain('Server-side pull request merging is disabled.')
114 114
115 115 if range_diff == "1":
116 116 response.mustcontain('Turn off: Show the diff as commit range')
117 117
118 118 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
119 119 # Logout
120 120 response = self.app.post(
121 121 h.route_path('logout'),
122 122 params={'csrf_token': csrf_token})
123 123 # Login as regular user
124 124 response = self.app.post(h.route_path('login'),
125 125 {'username': TEST_USER_REGULAR_LOGIN,
126 126 'password': 'test12'})
127 127
128 128 pull_request = pr_util.create_pull_request(
129 129 author=TEST_USER_REGULAR_LOGIN)
130 130
131 131 response = self.app.get(route_path(
132 132 'pullrequest_show',
133 133 repo_name=pull_request.target_repo.scm_instance().name,
134 134 pull_request_id=pull_request.pull_request_id))
135 135
136 136 response.mustcontain('Server-side pull request merging is disabled.')
137 137
138 138 assert_response = response.assert_response()
139 139 # for regular user without a merge permissions, we don't see it
140 140 assert_response.no_element_exists('#close-pull-request-action')
141 141
142 142 user_util.grant_user_permission_to_repo(
143 143 pull_request.target_repo,
144 144 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
145 145 'repository.write')
146 146 response = self.app.get(route_path(
147 147 'pullrequest_show',
148 148 repo_name=pull_request.target_repo.scm_instance().name,
149 149 pull_request_id=pull_request.pull_request_id))
150 150
151 151 response.mustcontain('Server-side pull request merging is disabled.')
152 152
153 153 assert_response = response.assert_response()
154 154 # now regular user has a merge permissions, we have CLOSE button
155 155 assert_response.one_element_exists('#close-pull-request-action')
156 156
157 157 def test_show_invalid_commit_id(self, pr_util):
158 158 # Simulating invalid revisions which will cause a lookup error
159 159 pull_request = pr_util.create_pull_request()
160 160 pull_request.revisions = ['invalid']
161 161 Session().add(pull_request)
162 162 Session().commit()
163 163
164 164 response = self.app.get(route_path(
165 165 'pullrequest_show',
166 166 repo_name=pull_request.target_repo.scm_instance().name,
167 167 pull_request_id=pull_request.pull_request_id))
168 168
169 169 for commit_id in pull_request.revisions:
170 170 response.mustcontain(commit_id)
171 171
172 172 def test_show_invalid_source_reference(self, pr_util):
173 173 pull_request = pr_util.create_pull_request()
174 174 pull_request.source_ref = 'branch:b:invalid'
175 175 Session().add(pull_request)
176 176 Session().commit()
177 177
178 178 self.app.get(route_path(
179 179 'pullrequest_show',
180 180 repo_name=pull_request.target_repo.scm_instance().name,
181 181 pull_request_id=pull_request.pull_request_id))
182 182
183 183 def test_edit_title_description(self, pr_util, csrf_token):
184 184 pull_request = pr_util.create_pull_request()
185 185 pull_request_id = pull_request.pull_request_id
186 186
187 187 response = self.app.post(
188 188 route_path('pullrequest_update',
189 189 repo_name=pull_request.target_repo.repo_name,
190 190 pull_request_id=pull_request_id),
191 191 params={
192 192 'edit_pull_request': 'true',
193 193 'title': 'New title',
194 194 'description': 'New description',
195 195 'csrf_token': csrf_token})
196 196
197 197 assert_session_flash(
198 198 response, u'Pull request title & description updated.',
199 199 category='success')
200 200
201 201 pull_request = PullRequest.get(pull_request_id)
202 202 assert pull_request.title == 'New title'
203 203 assert pull_request.description == 'New description'
204 204
205 205 def test_edit_title_description_closed(self, pr_util, csrf_token):
206 206 pull_request = pr_util.create_pull_request()
207 207 pull_request_id = pull_request.pull_request_id
208 208 repo_name = pull_request.target_repo.repo_name
209 209 pr_util.close()
210 210
211 211 response = self.app.post(
212 212 route_path('pullrequest_update',
213 213 repo_name=repo_name, pull_request_id=pull_request_id),
214 214 params={
215 215 'edit_pull_request': 'true',
216 216 'title': 'New title',
217 217 'description': 'New description',
218 218 'csrf_token': csrf_token}, status=200)
219 219 assert_session_flash(
220 220 response, u'Cannot update closed pull requests.',
221 221 category='error')
222 222
223 223 def test_update_invalid_source_reference(self, pr_util, csrf_token):
224 224 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
225 225
226 226 pull_request = pr_util.create_pull_request()
227 227 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
228 228 Session().add(pull_request)
229 229 Session().commit()
230 230
231 231 pull_request_id = pull_request.pull_request_id
232 232
233 233 response = self.app.post(
234 234 route_path('pullrequest_update',
235 235 repo_name=pull_request.target_repo.repo_name,
236 236 pull_request_id=pull_request_id),
237 237 params={'update_commits': 'true', 'csrf_token': csrf_token})
238 238
239 239 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 240 UpdateFailureReason.MISSING_SOURCE_REF])
241 241 assert_session_flash(response, expected_msg, category='error')
242 242
243 243 def test_missing_target_reference(self, pr_util, csrf_token):
244 244 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 245 pull_request = pr_util.create_pull_request(
246 246 approved=True, mergeable=True)
247 247 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
248 248 pull_request.target_ref = unicode_reference
249 249 Session().add(pull_request)
250 250 Session().commit()
251 251
252 252 pull_request_id = pull_request.pull_request_id
253 253 pull_request_url = route_path(
254 254 'pullrequest_show',
255 255 repo_name=pull_request.target_repo.repo_name,
256 256 pull_request_id=pull_request_id)
257 257
258 258 response = self.app.get(pull_request_url)
259 259 target_ref_id = 'invalid-branch'
260 260 merge_resp = MergeResponse(
261 261 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
262 262 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
263 263 response.assert_response().element_contains(
264 264 'span[data-role="merge-message"]', merge_resp.merge_status_message)
265 265
266 266 def test_comment_and_close_pull_request_custom_message_approved(
267 267 self, pr_util, csrf_token, xhr_header):
268 268
269 269 pull_request = pr_util.create_pull_request(approved=True)
270 270 pull_request_id = pull_request.pull_request_id
271 271 author = pull_request.user_id
272 272 repo = pull_request.target_repo.repo_id
273 273
274 274 self.app.post(
275 275 route_path('pullrequest_comment_create',
276 276 repo_name=pull_request.target_repo.scm_instance().name,
277 277 pull_request_id=pull_request_id),
278 278 params={
279 279 'close_pull_request': '1',
280 280 'text': 'Closing a PR',
281 281 'csrf_token': csrf_token},
282 282 extra_environ=xhr_header,)
283 283
284 284 journal = UserLog.query()\
285 285 .filter(UserLog.user_id == author)\
286 286 .filter(UserLog.repository_id == repo) \
287 .order_by('user_log_id') \
287 .order_by(UserLog.user_log_id.asc()) \
288 288 .all()
289 289 assert journal[-1].action == 'repo.pull_request.close'
290 290
291 291 pull_request = PullRequest.get(pull_request_id)
292 292 assert pull_request.is_closed()
293 293
294 294 status = ChangesetStatusModel().get_status(
295 295 pull_request.source_repo, pull_request=pull_request)
296 296 assert status == ChangesetStatus.STATUS_APPROVED
297 297 comments = ChangesetComment().query() \
298 298 .filter(ChangesetComment.pull_request == pull_request) \
299 299 .order_by(ChangesetComment.comment_id.asc())\
300 300 .all()
301 301 assert comments[-1].text == 'Closing a PR'
302 302
303 303 def test_comment_force_close_pull_request_rejected(
304 304 self, pr_util, csrf_token, xhr_header):
305 305 pull_request = pr_util.create_pull_request()
306 306 pull_request_id = pull_request.pull_request_id
307 307 PullRequestModel().update_reviewers(
308 308 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
309 309 pull_request.author)
310 310 author = pull_request.user_id
311 311 repo = pull_request.target_repo.repo_id
312 312
313 313 self.app.post(
314 314 route_path('pullrequest_comment_create',
315 315 repo_name=pull_request.target_repo.scm_instance().name,
316 316 pull_request_id=pull_request_id),
317 317 params={
318 318 'close_pull_request': '1',
319 319 'csrf_token': csrf_token},
320 320 extra_environ=xhr_header)
321 321
322 322 pull_request = PullRequest.get(pull_request_id)
323 323
324 324 journal = UserLog.query()\
325 325 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
326 .order_by('user_log_id') \
326 .order_by(UserLog.user_log_id.asc()) \
327 327 .all()
328 328 assert journal[-1].action == 'repo.pull_request.close'
329 329
330 330 # check only the latest status, not the review status
331 331 status = ChangesetStatusModel().get_status(
332 332 pull_request.source_repo, pull_request=pull_request)
333 333 assert status == ChangesetStatus.STATUS_REJECTED
334 334
335 335 def test_comment_and_close_pull_request(
336 336 self, pr_util, csrf_token, xhr_header):
337 337 pull_request = pr_util.create_pull_request()
338 338 pull_request_id = pull_request.pull_request_id
339 339
340 340 response = self.app.post(
341 341 route_path('pullrequest_comment_create',
342 342 repo_name=pull_request.target_repo.scm_instance().name,
343 343 pull_request_id=pull_request.pull_request_id),
344 344 params={
345 345 'close_pull_request': 'true',
346 346 'csrf_token': csrf_token},
347 347 extra_environ=xhr_header)
348 348
349 349 assert response.json
350 350
351 351 pull_request = PullRequest.get(pull_request_id)
352 352 assert pull_request.is_closed()
353 353
354 354 # check only the latest status, not the review status
355 355 status = ChangesetStatusModel().get_status(
356 356 pull_request.source_repo, pull_request=pull_request)
357 357 assert status == ChangesetStatus.STATUS_REJECTED
358 358
359 359 def test_create_pull_request(self, backend, csrf_token):
360 360 commits = [
361 361 {'message': 'ancestor'},
362 362 {'message': 'change'},
363 363 {'message': 'change2'},
364 364 ]
365 365 commit_ids = backend.create_master_repo(commits)
366 366 target = backend.create_repo(heads=['ancestor'])
367 367 source = backend.create_repo(heads=['change2'])
368 368
369 369 response = self.app.post(
370 370 route_path('pullrequest_create', repo_name=source.repo_name),
371 371 [
372 372 ('source_repo', source.repo_name),
373 373 ('source_ref', 'branch:default:' + commit_ids['change2']),
374 374 ('target_repo', target.repo_name),
375 375 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
376 376 ('common_ancestor', commit_ids['ancestor']),
377 377 ('pullrequest_title', 'Title'),
378 378 ('pullrequest_desc', 'Description'),
379 379 ('description_renderer', 'markdown'),
380 380 ('__start__', 'review_members:sequence'),
381 381 ('__start__', 'reviewer:mapping'),
382 382 ('user_id', '1'),
383 383 ('__start__', 'reasons:sequence'),
384 384 ('reason', 'Some reason'),
385 385 ('__end__', 'reasons:sequence'),
386 386 ('__start__', 'rules:sequence'),
387 387 ('__end__', 'rules:sequence'),
388 388 ('mandatory', 'False'),
389 389 ('__end__', 'reviewer:mapping'),
390 390 ('__end__', 'review_members:sequence'),
391 391 ('__start__', 'revisions:sequence'),
392 392 ('revisions', commit_ids['change']),
393 393 ('revisions', commit_ids['change2']),
394 394 ('__end__', 'revisions:sequence'),
395 395 ('user', ''),
396 396 ('csrf_token', csrf_token),
397 397 ],
398 398 status=302)
399 399
400 400 location = response.headers['Location']
401 401 pull_request_id = location.rsplit('/', 1)[1]
402 402 assert pull_request_id != 'new'
403 403 pull_request = PullRequest.get(int(pull_request_id))
404 404
405 405 # check that we have now both revisions
406 406 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
407 407 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
408 408 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
409 409 assert pull_request.target_ref == expected_target_ref
410 410
411 411 def test_reviewer_notifications(self, backend, csrf_token):
412 412 # We have to use the app.post for this test so it will create the
413 413 # notifications properly with the new PR
414 414 commits = [
415 415 {'message': 'ancestor',
416 416 'added': [FileNode('file_A', content='content_of_ancestor')]},
417 417 {'message': 'change',
418 418 'added': [FileNode('file_a', content='content_of_change')]},
419 419 {'message': 'change-child'},
420 420 {'message': 'ancestor-child', 'parents': ['ancestor'],
421 421 'added': [
422 422 FileNode('file_B', content='content_of_ancestor_child')]},
423 423 {'message': 'ancestor-child-2'},
424 424 ]
425 425 commit_ids = backend.create_master_repo(commits)
426 426 target = backend.create_repo(heads=['ancestor-child'])
427 427 source = backend.create_repo(heads=['change'])
428 428
429 429 response = self.app.post(
430 430 route_path('pullrequest_create', repo_name=source.repo_name),
431 431 [
432 432 ('source_repo', source.repo_name),
433 433 ('source_ref', 'branch:default:' + commit_ids['change']),
434 434 ('target_repo', target.repo_name),
435 435 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
436 436 ('common_ancestor', commit_ids['ancestor']),
437 437 ('pullrequest_title', 'Title'),
438 438 ('pullrequest_desc', 'Description'),
439 439 ('description_renderer', 'markdown'),
440 440 ('__start__', 'review_members:sequence'),
441 441 ('__start__', 'reviewer:mapping'),
442 442 ('user_id', '2'),
443 443 ('__start__', 'reasons:sequence'),
444 444 ('reason', 'Some reason'),
445 445 ('__end__', 'reasons:sequence'),
446 446 ('__start__', 'rules:sequence'),
447 447 ('__end__', 'rules:sequence'),
448 448 ('mandatory', 'False'),
449 449 ('__end__', 'reviewer:mapping'),
450 450 ('__end__', 'review_members:sequence'),
451 451 ('__start__', 'revisions:sequence'),
452 452 ('revisions', commit_ids['change']),
453 453 ('__end__', 'revisions:sequence'),
454 454 ('user', ''),
455 455 ('csrf_token', csrf_token),
456 456 ],
457 457 status=302)
458 458
459 459 location = response.headers['Location']
460 460
461 461 pull_request_id = location.rsplit('/', 1)[1]
462 462 assert pull_request_id != 'new'
463 463 pull_request = PullRequest.get(int(pull_request_id))
464 464
465 465 # Check that a notification was made
466 466 notifications = Notification.query()\
467 467 .filter(Notification.created_by == pull_request.author.user_id,
468 468 Notification.type_ == Notification.TYPE_PULL_REQUEST,
469 469 Notification.subject.contains(
470 470 "wants you to review pull request #%s" % pull_request_id))
471 471 assert len(notifications.all()) == 1
472 472
473 473 # Change reviewers and check that a notification was made
474 474 PullRequestModel().update_reviewers(
475 475 pull_request.pull_request_id, [(1, [], False, [])],
476 476 pull_request.author)
477 477 assert len(notifications.all()) == 2
478 478
479 479 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
480 480 csrf_token):
481 481 commits = [
482 482 {'message': 'ancestor',
483 483 'added': [FileNode('file_A', content='content_of_ancestor')]},
484 484 {'message': 'change',
485 485 'added': [FileNode('file_a', content='content_of_change')]},
486 486 {'message': 'change-child'},
487 487 {'message': 'ancestor-child', 'parents': ['ancestor'],
488 488 'added': [
489 489 FileNode('file_B', content='content_of_ancestor_child')]},
490 490 {'message': 'ancestor-child-2'},
491 491 ]
492 492 commit_ids = backend.create_master_repo(commits)
493 493 target = backend.create_repo(heads=['ancestor-child'])
494 494 source = backend.create_repo(heads=['change'])
495 495
496 496 response = self.app.post(
497 497 route_path('pullrequest_create', repo_name=source.repo_name),
498 498 [
499 499 ('source_repo', source.repo_name),
500 500 ('source_ref', 'branch:default:' + commit_ids['change']),
501 501 ('target_repo', target.repo_name),
502 502 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
503 503 ('common_ancestor', commit_ids['ancestor']),
504 504 ('pullrequest_title', 'Title'),
505 505 ('pullrequest_desc', 'Description'),
506 506 ('description_renderer', 'markdown'),
507 507 ('__start__', 'review_members:sequence'),
508 508 ('__start__', 'reviewer:mapping'),
509 509 ('user_id', '1'),
510 510 ('__start__', 'reasons:sequence'),
511 511 ('reason', 'Some reason'),
512 512 ('__end__', 'reasons:sequence'),
513 513 ('__start__', 'rules:sequence'),
514 514 ('__end__', 'rules:sequence'),
515 515 ('mandatory', 'False'),
516 516 ('__end__', 'reviewer:mapping'),
517 517 ('__end__', 'review_members:sequence'),
518 518 ('__start__', 'revisions:sequence'),
519 519 ('revisions', commit_ids['change']),
520 520 ('__end__', 'revisions:sequence'),
521 521 ('user', ''),
522 522 ('csrf_token', csrf_token),
523 523 ],
524 524 status=302)
525 525
526 526 location = response.headers['Location']
527 527
528 528 pull_request_id = location.rsplit('/', 1)[1]
529 529 assert pull_request_id != 'new'
530 530 pull_request = PullRequest.get(int(pull_request_id))
531 531
532 532 # target_ref has to point to the ancestor's commit_id in order to
533 533 # show the correct diff
534 534 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
535 535 assert pull_request.target_ref == expected_target_ref
536 536
537 537 # Check generated diff contents
538 538 response = response.follow()
539 539 assert 'content_of_ancestor' not in response.body
540 540 assert 'content_of_ancestor-child' not in response.body
541 541 assert 'content_of_change' in response.body
542 542
543 543 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
544 544 # Clear any previous calls to rcextensions
545 545 rhodecode.EXTENSIONS.calls.clear()
546 546
547 547 pull_request = pr_util.create_pull_request(
548 548 approved=True, mergeable=True)
549 549 pull_request_id = pull_request.pull_request_id
550 550 repo_name = pull_request.target_repo.scm_instance().name,
551 551
552 552 response = self.app.post(
553 553 route_path('pullrequest_merge',
554 554 repo_name=str(repo_name[0]),
555 555 pull_request_id=pull_request_id),
556 556 params={'csrf_token': csrf_token}).follow()
557 557
558 558 pull_request = PullRequest.get(pull_request_id)
559 559
560 560 assert response.status_int == 200
561 561 assert pull_request.is_closed()
562 562 assert_pull_request_status(
563 563 pull_request, ChangesetStatus.STATUS_APPROVED)
564 564
565 565 # Check the relevant log entries were added
566 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
566 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
567 567 actions = [log.action for log in user_logs]
568 568 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
569 569 expected_actions = [
570 570 u'repo.pull_request.close',
571 571 u'repo.pull_request.merge',
572 572 u'repo.pull_request.comment.create'
573 573 ]
574 574 assert actions == expected_actions
575 575
576 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
576 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
577 577 actions = [log for log in user_logs]
578 578 assert actions[-1].action == 'user.push'
579 579 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
580 580
581 581 # Check post_push rcextension was really executed
582 582 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
583 583 assert len(push_calls) == 1
584 584 unused_last_call_args, last_call_kwargs = push_calls[0]
585 585 assert last_call_kwargs['action'] == 'push'
586 586 assert last_call_kwargs['commit_ids'] == pr_commit_ids
587 587
588 588 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
589 589 pull_request = pr_util.create_pull_request(mergeable=False)
590 590 pull_request_id = pull_request.pull_request_id
591 591 pull_request = PullRequest.get(pull_request_id)
592 592
593 593 response = self.app.post(
594 594 route_path('pullrequest_merge',
595 595 repo_name=pull_request.target_repo.scm_instance().name,
596 596 pull_request_id=pull_request.pull_request_id),
597 597 params={'csrf_token': csrf_token}).follow()
598 598
599 599 assert response.status_int == 200
600 600 response.mustcontain(
601 601 'Merge is not currently possible because of below failed checks.')
602 602 response.mustcontain('Server-side pull request merging is disabled.')
603 603
604 604 @pytest.mark.skip_backends('svn')
605 605 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
606 606 pull_request = pr_util.create_pull_request(mergeable=True)
607 607 pull_request_id = pull_request.pull_request_id
608 608 repo_name = pull_request.target_repo.scm_instance().name
609 609
610 610 response = self.app.post(
611 611 route_path('pullrequest_merge',
612 612 repo_name=repo_name, pull_request_id=pull_request_id),
613 613 params={'csrf_token': csrf_token}).follow()
614 614
615 615 assert response.status_int == 200
616 616
617 617 response.mustcontain(
618 618 'Merge is not currently possible because of below failed checks.')
619 619 response.mustcontain('Pull request reviewer approval is pending.')
620 620
621 621 def test_merge_pull_request_renders_failure_reason(
622 622 self, user_regular, csrf_token, pr_util):
623 623 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
624 624 pull_request_id = pull_request.pull_request_id
625 625 repo_name = pull_request.target_repo.scm_instance().name
626 626
627 627 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
628 628 MergeFailureReason.PUSH_FAILED,
629 629 metadata={'target': 'shadow repo',
630 630 'merge_commit': 'xxx'})
631 631 model_patcher = mock.patch.multiple(
632 632 PullRequestModel,
633 633 merge_repo=mock.Mock(return_value=merge_resp),
634 634 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
635 635
636 636 with model_patcher:
637 637 response = self.app.post(
638 638 route_path('pullrequest_merge',
639 639 repo_name=repo_name,
640 640 pull_request_id=pull_request_id),
641 641 params={'csrf_token': csrf_token}, status=302)
642 642
643 643 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
644 644 metadata={'target': 'shadow repo',
645 645 'merge_commit': 'xxx'})
646 646 assert_session_flash(response, merge_resp.merge_status_message)
647 647
648 648 def test_update_source_revision(self, backend, csrf_token):
649 649 commits = [
650 650 {'message': 'ancestor'},
651 651 {'message': 'change'},
652 652 {'message': 'change-2'},
653 653 ]
654 654 commit_ids = backend.create_master_repo(commits)
655 655 target = backend.create_repo(heads=['ancestor'])
656 656 source = backend.create_repo(heads=['change'])
657 657
658 658 # create pr from a in source to A in target
659 659 pull_request = PullRequest()
660 660
661 661 pull_request.source_repo = source
662 662 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
663 663 branch=backend.default_branch_name, commit_id=commit_ids['change'])
664 664
665 665 pull_request.target_repo = target
666 666 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
667 667 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
668 668
669 669 pull_request.revisions = [commit_ids['change']]
670 670 pull_request.title = u"Test"
671 671 pull_request.description = u"Description"
672 672 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
673 673 pull_request.pull_request_state = PullRequest.STATE_CREATED
674 674 Session().add(pull_request)
675 675 Session().commit()
676 676 pull_request_id = pull_request.pull_request_id
677 677
678 678 # source has ancestor - change - change-2
679 679 backend.pull_heads(source, heads=['change-2'])
680 680
681 681 # update PR
682 682 self.app.post(
683 683 route_path('pullrequest_update',
684 684 repo_name=target.repo_name, pull_request_id=pull_request_id),
685 685 params={'update_commits': 'true', 'csrf_token': csrf_token})
686 686
687 687 response = self.app.get(
688 688 route_path('pullrequest_show',
689 689 repo_name=target.repo_name,
690 690 pull_request_id=pull_request.pull_request_id))
691 691
692 692 assert response.status_int == 200
693 693 assert 'Pull request updated to' in response.body
694 694 assert 'with 1 added, 0 removed commits.' in response.body
695 695
696 696 # check that we have now both revisions
697 697 pull_request = PullRequest.get(pull_request_id)
698 698 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
699 699
700 700 def test_update_target_revision(self, backend, csrf_token):
701 701 commits = [
702 702 {'message': 'ancestor'},
703 703 {'message': 'change'},
704 704 {'message': 'ancestor-new', 'parents': ['ancestor']},
705 705 {'message': 'change-rebased'},
706 706 ]
707 707 commit_ids = backend.create_master_repo(commits)
708 708 target = backend.create_repo(heads=['ancestor'])
709 709 source = backend.create_repo(heads=['change'])
710 710
711 711 # create pr from a in source to A in target
712 712 pull_request = PullRequest()
713 713
714 714 pull_request.source_repo = source
715 715 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
716 716 branch=backend.default_branch_name, commit_id=commit_ids['change'])
717 717
718 718 pull_request.target_repo = target
719 719 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
720 720 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
721 721
722 722 pull_request.revisions = [commit_ids['change']]
723 723 pull_request.title = u"Test"
724 724 pull_request.description = u"Description"
725 725 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
726 726 pull_request.pull_request_state = PullRequest.STATE_CREATED
727 727
728 728 Session().add(pull_request)
729 729 Session().commit()
730 730 pull_request_id = pull_request.pull_request_id
731 731
732 732 # target has ancestor - ancestor-new
733 733 # source has ancestor - ancestor-new - change-rebased
734 734 backend.pull_heads(target, heads=['ancestor-new'])
735 735 backend.pull_heads(source, heads=['change-rebased'])
736 736
737 737 # update PR
738 738 self.app.post(
739 739 route_path('pullrequest_update',
740 740 repo_name=target.repo_name,
741 741 pull_request_id=pull_request_id),
742 742 params={'update_commits': 'true', 'csrf_token': csrf_token},
743 743 status=200)
744 744
745 745 # check that we have now both revisions
746 746 pull_request = PullRequest.get(pull_request_id)
747 747 assert pull_request.revisions == [commit_ids['change-rebased']]
748 748 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
749 749 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
750 750
751 751 response = self.app.get(
752 752 route_path('pullrequest_show',
753 753 repo_name=target.repo_name,
754 754 pull_request_id=pull_request.pull_request_id))
755 755 assert response.status_int == 200
756 756 assert 'Pull request updated to' in response.body
757 757 assert 'with 1 added, 1 removed commits.' in response.body
758 758
759 759 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
760 760 backend = backend_git
761 761 commits = [
762 762 {'message': 'master-commit-1'},
763 763 {'message': 'master-commit-2-change-1'},
764 764 {'message': 'master-commit-3-change-2'},
765 765
766 766 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
767 767 {'message': 'feat-commit-2'},
768 768 ]
769 769 commit_ids = backend.create_master_repo(commits)
770 770 target = backend.create_repo(heads=['master-commit-3-change-2'])
771 771 source = backend.create_repo(heads=['feat-commit-2'])
772 772
773 773 # create pr from a in source to A in target
774 774 pull_request = PullRequest()
775 775 pull_request.source_repo = source
776 776
777 777 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
778 778 branch=backend.default_branch_name,
779 779 commit_id=commit_ids['master-commit-3-change-2'])
780 780
781 781 pull_request.target_repo = target
782 782 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
783 783 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
784 784
785 785 pull_request.revisions = [
786 786 commit_ids['feat-commit-1'],
787 787 commit_ids['feat-commit-2']
788 788 ]
789 789 pull_request.title = u"Test"
790 790 pull_request.description = u"Description"
791 791 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
792 792 pull_request.pull_request_state = PullRequest.STATE_CREATED
793 793 Session().add(pull_request)
794 794 Session().commit()
795 795 pull_request_id = pull_request.pull_request_id
796 796
797 797 # PR is created, now we simulate a force-push into target,
798 798 # that drops a 2 last commits
799 799 vcsrepo = target.scm_instance()
800 800 vcsrepo.config.clear_section('hooks')
801 801 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
802 802
803 803 # update PR
804 804 self.app.post(
805 805 route_path('pullrequest_update',
806 806 repo_name=target.repo_name,
807 807 pull_request_id=pull_request_id),
808 808 params={'update_commits': 'true', 'csrf_token': csrf_token},
809 809 status=200)
810 810
811 811 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
812 812 assert response.status_int == 200
813 813 response.mustcontain('Pull request updated to')
814 814 response.mustcontain('with 0 added, 0 removed commits.')
815 815
816 816 def test_update_of_ancestor_reference(self, backend, csrf_token):
817 817 commits = [
818 818 {'message': 'ancestor'},
819 819 {'message': 'change'},
820 820 {'message': 'change-2'},
821 821 {'message': 'ancestor-new', 'parents': ['ancestor']},
822 822 {'message': 'change-rebased'},
823 823 ]
824 824 commit_ids = backend.create_master_repo(commits)
825 825 target = backend.create_repo(heads=['ancestor'])
826 826 source = backend.create_repo(heads=['change'])
827 827
828 828 # create pr from a in source to A in target
829 829 pull_request = PullRequest()
830 830 pull_request.source_repo = source
831 831
832 832 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
833 833 branch=backend.default_branch_name, commit_id=commit_ids['change'])
834 834 pull_request.target_repo = target
835 835 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
836 836 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
837 837 pull_request.revisions = [commit_ids['change']]
838 838 pull_request.title = u"Test"
839 839 pull_request.description = u"Description"
840 840 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
841 841 pull_request.pull_request_state = PullRequest.STATE_CREATED
842 842 Session().add(pull_request)
843 843 Session().commit()
844 844 pull_request_id = pull_request.pull_request_id
845 845
846 846 # target has ancestor - ancestor-new
847 847 # source has ancestor - ancestor-new - change-rebased
848 848 backend.pull_heads(target, heads=['ancestor-new'])
849 849 backend.pull_heads(source, heads=['change-rebased'])
850 850
851 851 # update PR
852 852 self.app.post(
853 853 route_path('pullrequest_update',
854 854 repo_name=target.repo_name, pull_request_id=pull_request_id),
855 855 params={'update_commits': 'true', 'csrf_token': csrf_token},
856 856 status=200)
857 857
858 858 # Expect the target reference to be updated correctly
859 859 pull_request = PullRequest.get(pull_request_id)
860 860 assert pull_request.revisions == [commit_ids['change-rebased']]
861 861 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
862 862 branch=backend.default_branch_name,
863 863 commit_id=commit_ids['ancestor-new'])
864 864 assert pull_request.target_ref == expected_target_ref
865 865
866 866 def test_remove_pull_request_branch(self, backend_git, csrf_token):
867 867 branch_name = 'development'
868 868 commits = [
869 869 {'message': 'initial-commit'},
870 870 {'message': 'old-feature'},
871 871 {'message': 'new-feature', 'branch': branch_name},
872 872 ]
873 873 repo = backend_git.create_repo(commits)
874 874 repo_name = repo.repo_name
875 875 commit_ids = backend_git.commit_ids
876 876
877 877 pull_request = PullRequest()
878 878 pull_request.source_repo = repo
879 879 pull_request.target_repo = repo
880 880 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
881 881 branch=branch_name, commit_id=commit_ids['new-feature'])
882 882 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
883 883 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
884 884 pull_request.revisions = [commit_ids['new-feature']]
885 885 pull_request.title = u"Test"
886 886 pull_request.description = u"Description"
887 887 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
888 888 pull_request.pull_request_state = PullRequest.STATE_CREATED
889 889 Session().add(pull_request)
890 890 Session().commit()
891 891
892 892 pull_request_id = pull_request.pull_request_id
893 893
894 894 vcs = repo.scm_instance()
895 895 vcs.remove_ref('refs/heads/{}'.format(branch_name))
896 896
897 897 response = self.app.get(route_path(
898 898 'pullrequest_show',
899 899 repo_name=repo_name,
900 900 pull_request_id=pull_request_id))
901 901
902 902 assert response.status_int == 200
903 903
904 904 response.assert_response().element_contains(
905 905 '#changeset_compare_view_content .alert strong',
906 906 'Missing commits')
907 907 response.assert_response().element_contains(
908 908 '#changeset_compare_view_content .alert',
909 909 'This pull request cannot be displayed, because one or more'
910 910 ' commits no longer exist in the source repository.')
911 911
912 912 def test_strip_commits_from_pull_request(
913 913 self, backend, pr_util, csrf_token):
914 914 commits = [
915 915 {'message': 'initial-commit'},
916 916 {'message': 'old-feature'},
917 917 {'message': 'new-feature', 'parents': ['initial-commit']},
918 918 ]
919 919 pull_request = pr_util.create_pull_request(
920 920 commits, target_head='initial-commit', source_head='new-feature',
921 921 revisions=['new-feature'])
922 922
923 923 vcs = pr_util.source_repository.scm_instance()
924 924 if backend.alias == 'git':
925 925 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
926 926 else:
927 927 vcs.strip(pr_util.commit_ids['new-feature'])
928 928
929 929 response = self.app.get(route_path(
930 930 'pullrequest_show',
931 931 repo_name=pr_util.target_repository.repo_name,
932 932 pull_request_id=pull_request.pull_request_id))
933 933
934 934 assert response.status_int == 200
935 935
936 936 response.assert_response().element_contains(
937 937 '#changeset_compare_view_content .alert strong',
938 938 'Missing commits')
939 939 response.assert_response().element_contains(
940 940 '#changeset_compare_view_content .alert',
941 941 'This pull request cannot be displayed, because one or more'
942 942 ' commits no longer exist in the source repository.')
943 943 response.assert_response().element_contains(
944 944 '#update_commits',
945 945 'Update commits')
946 946
947 947 def test_strip_commits_and_update(
948 948 self, backend, pr_util, csrf_token):
949 949 commits = [
950 950 {'message': 'initial-commit'},
951 951 {'message': 'old-feature'},
952 952 {'message': 'new-feature', 'parents': ['old-feature']},
953 953 ]
954 954 pull_request = pr_util.create_pull_request(
955 955 commits, target_head='old-feature', source_head='new-feature',
956 956 revisions=['new-feature'], mergeable=True)
957 957
958 958 vcs = pr_util.source_repository.scm_instance()
959 959 if backend.alias == 'git':
960 960 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
961 961 else:
962 962 vcs.strip(pr_util.commit_ids['new-feature'])
963 963
964 964 response = self.app.post(
965 965 route_path('pullrequest_update',
966 966 repo_name=pull_request.target_repo.repo_name,
967 967 pull_request_id=pull_request.pull_request_id),
968 968 params={'update_commits': 'true',
969 969 'csrf_token': csrf_token})
970 970
971 971 assert response.status_int == 200
972 972 assert response.body == 'true'
973 973
974 974 # Make sure that after update, it won't raise 500 errors
975 975 response = self.app.get(route_path(
976 976 'pullrequest_show',
977 977 repo_name=pr_util.target_repository.repo_name,
978 978 pull_request_id=pull_request.pull_request_id))
979 979
980 980 assert response.status_int == 200
981 981 response.assert_response().element_contains(
982 982 '#changeset_compare_view_content .alert strong',
983 983 'Missing commits')
984 984
985 985 def test_branch_is_a_link(self, pr_util):
986 986 pull_request = pr_util.create_pull_request()
987 987 pull_request.source_ref = 'branch:origin:1234567890abcdef'
988 988 pull_request.target_ref = 'branch:target:abcdef1234567890'
989 989 Session().add(pull_request)
990 990 Session().commit()
991 991
992 992 response = self.app.get(route_path(
993 993 'pullrequest_show',
994 994 repo_name=pull_request.target_repo.scm_instance().name,
995 995 pull_request_id=pull_request.pull_request_id))
996 996 assert response.status_int == 200
997 997
998 998 origin = response.assert_response().get_element('.pr-origininfo .tag')
999 999 origin_children = origin.getchildren()
1000 1000 assert len(origin_children) == 1
1001 1001 target = response.assert_response().get_element('.pr-targetinfo .tag')
1002 1002 target_children = target.getchildren()
1003 1003 assert len(target_children) == 1
1004 1004
1005 1005 expected_origin_link = route_path(
1006 1006 'repo_commits',
1007 1007 repo_name=pull_request.source_repo.scm_instance().name,
1008 1008 params=dict(branch='origin'))
1009 1009 expected_target_link = route_path(
1010 1010 'repo_commits',
1011 1011 repo_name=pull_request.target_repo.scm_instance().name,
1012 1012 params=dict(branch='target'))
1013 1013 assert origin_children[0].attrib['href'] == expected_origin_link
1014 1014 assert origin_children[0].text == 'branch: origin'
1015 1015 assert target_children[0].attrib['href'] == expected_target_link
1016 1016 assert target_children[0].text == 'branch: target'
1017 1017
1018 1018 def test_bookmark_is_not_a_link(self, pr_util):
1019 1019 pull_request = pr_util.create_pull_request()
1020 1020 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1021 1021 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1022 1022 Session().add(pull_request)
1023 1023 Session().commit()
1024 1024
1025 1025 response = self.app.get(route_path(
1026 1026 'pullrequest_show',
1027 1027 repo_name=pull_request.target_repo.scm_instance().name,
1028 1028 pull_request_id=pull_request.pull_request_id))
1029 1029 assert response.status_int == 200
1030 1030
1031 1031 origin = response.assert_response().get_element('.pr-origininfo .tag')
1032 1032 assert origin.text.strip() == 'bookmark: origin'
1033 1033 assert origin.getchildren() == []
1034 1034
1035 1035 target = response.assert_response().get_element('.pr-targetinfo .tag')
1036 1036 assert target.text.strip() == 'bookmark: target'
1037 1037 assert target.getchildren() == []
1038 1038
1039 1039 def test_tag_is_not_a_link(self, pr_util):
1040 1040 pull_request = pr_util.create_pull_request()
1041 1041 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1042 1042 pull_request.target_ref = 'tag:target:abcdef1234567890'
1043 1043 Session().add(pull_request)
1044 1044 Session().commit()
1045 1045
1046 1046 response = self.app.get(route_path(
1047 1047 'pullrequest_show',
1048 1048 repo_name=pull_request.target_repo.scm_instance().name,
1049 1049 pull_request_id=pull_request.pull_request_id))
1050 1050 assert response.status_int == 200
1051 1051
1052 1052 origin = response.assert_response().get_element('.pr-origininfo .tag')
1053 1053 assert origin.text.strip() == 'tag: origin'
1054 1054 assert origin.getchildren() == []
1055 1055
1056 1056 target = response.assert_response().get_element('.pr-targetinfo .tag')
1057 1057 assert target.text.strip() == 'tag: target'
1058 1058 assert target.getchildren() == []
1059 1059
1060 1060 @pytest.mark.parametrize('mergeable', [True, False])
1061 1061 def test_shadow_repository_link(
1062 1062 self, mergeable, pr_util, http_host_only_stub):
1063 1063 """
1064 1064 Check that the pull request summary page displays a link to the shadow
1065 1065 repository if the pull request is mergeable. If it is not mergeable
1066 1066 the link should not be displayed.
1067 1067 """
1068 1068 pull_request = pr_util.create_pull_request(
1069 1069 mergeable=mergeable, enable_notifications=False)
1070 1070 target_repo = pull_request.target_repo.scm_instance()
1071 1071 pr_id = pull_request.pull_request_id
1072 1072 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1073 1073 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1074 1074
1075 1075 response = self.app.get(route_path(
1076 1076 'pullrequest_show',
1077 1077 repo_name=target_repo.name,
1078 1078 pull_request_id=pr_id))
1079 1079
1080 1080 if mergeable:
1081 1081 response.assert_response().element_value_contains(
1082 1082 'input.pr-mergeinfo', shadow_url)
1083 1083 response.assert_response().element_value_contains(
1084 1084 'input.pr-mergeinfo ', 'pr-merge')
1085 1085 else:
1086 1086 response.assert_response().no_element_exists('.pr-mergeinfo')
1087 1087
1088 1088
1089 1089 @pytest.mark.usefixtures('app')
1090 1090 @pytest.mark.backends("git", "hg")
1091 1091 class TestPullrequestsControllerDelete(object):
1092 1092 def test_pull_request_delete_button_permissions_admin(
1093 1093 self, autologin_user, user_admin, pr_util):
1094 1094 pull_request = pr_util.create_pull_request(
1095 1095 author=user_admin.username, enable_notifications=False)
1096 1096
1097 1097 response = self.app.get(route_path(
1098 1098 'pullrequest_show',
1099 1099 repo_name=pull_request.target_repo.scm_instance().name,
1100 1100 pull_request_id=pull_request.pull_request_id))
1101 1101
1102 1102 response.mustcontain('id="delete_pullrequest"')
1103 1103 response.mustcontain('Confirm to delete this pull request')
1104 1104
1105 1105 def test_pull_request_delete_button_permissions_owner(
1106 1106 self, autologin_regular_user, user_regular, pr_util):
1107 1107 pull_request = pr_util.create_pull_request(
1108 1108 author=user_regular.username, enable_notifications=False)
1109 1109
1110 1110 response = self.app.get(route_path(
1111 1111 'pullrequest_show',
1112 1112 repo_name=pull_request.target_repo.scm_instance().name,
1113 1113 pull_request_id=pull_request.pull_request_id))
1114 1114
1115 1115 response.mustcontain('id="delete_pullrequest"')
1116 1116 response.mustcontain('Confirm to delete this pull request')
1117 1117
1118 1118 def test_pull_request_delete_button_permissions_forbidden(
1119 1119 self, autologin_regular_user, user_regular, user_admin, pr_util):
1120 1120 pull_request = pr_util.create_pull_request(
1121 1121 author=user_admin.username, enable_notifications=False)
1122 1122
1123 1123 response = self.app.get(route_path(
1124 1124 'pullrequest_show',
1125 1125 repo_name=pull_request.target_repo.scm_instance().name,
1126 1126 pull_request_id=pull_request.pull_request_id))
1127 1127 response.mustcontain(no=['id="delete_pullrequest"'])
1128 1128 response.mustcontain(no=['Confirm to delete this pull request'])
1129 1129
1130 1130 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1131 1131 self, autologin_regular_user, user_regular, user_admin, pr_util,
1132 1132 user_util):
1133 1133
1134 1134 pull_request = pr_util.create_pull_request(
1135 1135 author=user_admin.username, enable_notifications=False)
1136 1136
1137 1137 user_util.grant_user_permission_to_repo(
1138 1138 pull_request.target_repo, user_regular,
1139 1139 'repository.write')
1140 1140
1141 1141 response = self.app.get(route_path(
1142 1142 'pullrequest_show',
1143 1143 repo_name=pull_request.target_repo.scm_instance().name,
1144 1144 pull_request_id=pull_request.pull_request_id))
1145 1145
1146 1146 response.mustcontain('id="open_edit_pullrequest"')
1147 1147 response.mustcontain('id="delete_pullrequest"')
1148 1148 response.mustcontain(no=['Confirm to delete this pull request'])
1149 1149
1150 1150 def test_delete_comment_returns_404_if_comment_does_not_exist(
1151 1151 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1152 1152
1153 1153 pull_request = pr_util.create_pull_request(
1154 1154 author=user_admin.username, enable_notifications=False)
1155 1155
1156 1156 self.app.post(
1157 1157 route_path(
1158 1158 'pullrequest_comment_delete',
1159 1159 repo_name=pull_request.target_repo.scm_instance().name,
1160 1160 pull_request_id=pull_request.pull_request_id,
1161 1161 comment_id=1024404),
1162 1162 extra_environ=xhr_header,
1163 1163 params={'csrf_token': csrf_token},
1164 1164 status=404
1165 1165 )
1166 1166
1167 1167 def test_delete_comment(
1168 1168 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1169 1169
1170 1170 pull_request = pr_util.create_pull_request(
1171 1171 author=user_admin.username, enable_notifications=False)
1172 1172 comment = pr_util.create_comment()
1173 1173 comment_id = comment.comment_id
1174 1174
1175 1175 response = self.app.post(
1176 1176 route_path(
1177 1177 'pullrequest_comment_delete',
1178 1178 repo_name=pull_request.target_repo.scm_instance().name,
1179 1179 pull_request_id=pull_request.pull_request_id,
1180 1180 comment_id=comment_id),
1181 1181 extra_environ=xhr_header,
1182 1182 params={'csrf_token': csrf_token},
1183 1183 status=200
1184 1184 )
1185 1185 assert response.body == 'true'
1186 1186
1187 1187 @pytest.mark.parametrize('url_type', [
1188 1188 'pullrequest_new',
1189 1189 'pullrequest_create',
1190 1190 'pullrequest_update',
1191 1191 'pullrequest_merge',
1192 1192 ])
1193 1193 def test_pull_request_is_forbidden_on_archived_repo(
1194 1194 self, autologin_user, backend, xhr_header, user_util, url_type):
1195 1195
1196 1196 # create a temporary repo
1197 1197 source = user_util.create_repo(repo_type=backend.alias)
1198 1198 repo_name = source.repo_name
1199 1199 repo = Repository.get_by_repo_name(repo_name)
1200 1200 repo.archived = True
1201 1201 Session().commit()
1202 1202
1203 1203 response = self.app.get(
1204 1204 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1205 1205
1206 1206 msg = 'Action not supported for archived repository.'
1207 1207 assert_session_flash(response, msg)
1208 1208
1209 1209
1210 1210 def assert_pull_request_status(pull_request, expected_status):
1211 1211 status = ChangesetStatusModel().calculated_review_status(
1212 1212 pull_request=pull_request)
1213 1213 assert status == expected_status
1214 1214
1215 1215
1216 1216 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1217 1217 @pytest.mark.usefixtures("autologin_user")
1218 1218 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1219 1219 response = app.get(
1220 1220 route_path(route, repo_name=backend_svn.repo_name), status=404)
1221 1221
@@ -1,151 +1,152 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23 from rhodecode.model.db import Session, UserLog
24 24 from rhodecode.lib import hooks_base, utils2
25 25
26 26
27 27 def test_post_push_truncates_commits(user_regular, repo_stub):
28 28 extras = {
29 29 'ip': '127.0.0.1',
30 30 'username': user_regular.username,
31 31 'user_id': user_regular.user_id,
32 32 'action': 'push_local',
33 33 'repository': repo_stub.repo_name,
34 34 'scm': 'git',
35 35 'config': '',
36 36 'server_url': 'http://example.com',
37 37 'make_lock': None,
38 38 'user_agent': 'some-client',
39 39 'locked_by': [None],
40 40 'commit_ids': ['abcde12345' * 4] * 30000,
41 41 'hook_type': 'large_push_test_type',
42 42 'is_shadow_repo': False,
43 43 }
44 44 extras = utils2.AttributeDict(extras)
45 45
46 46 hooks_base.post_push(extras)
47 47
48 48 # Calculate appropriate action string here
49 49 commit_ids = extras.commit_ids[:400]
50 50
51 entry = UserLog.query().order_by('-user_log_id').first()
51 entry = UserLog.query().order_by(UserLog.user_log_id.desc()).first()
52 52 assert entry.action == 'user.push'
53 53 assert entry.action_data['commit_ids'] == commit_ids
54 54 Session().delete(entry)
55 55 Session().commit()
56 56
57 57
58 58 def assert_called_with_mock(callable_, expected_mock_name):
59 59 mock_obj = callable_.call_args[0][0]
60 60 mock_name = mock_obj._mock_new_parent._mock_new_name
61 61 assert mock_name == expected_mock_name
62 62
63 63
64 64 @pytest.fixture()
65 65 def hook_extras(user_regular, repo_stub):
66 66 extras = utils2.AttributeDict({
67 67 'ip': '127.0.0.1',
68 68 'username': user_regular.username,
69 69 'user_id': user_regular.user_id,
70 70 'action': 'push',
71 71 'repository': repo_stub.repo_name,
72 72 'scm': '',
73 73 'config': '',
74 74 'repo_store': '',
75 75 'server_url': 'http://example.com',
76 76 'make_lock': None,
77 77 'user_agent': 'some-client',
78 78 'locked_by': [None],
79 79 'commit_ids': [],
80 80 'hook_type': 'test_type',
81 81 'is_shadow_repo': False,
82 82 })
83 83 return extras
84 84
85 85
86 86 @pytest.mark.parametrize('func, extension, event', [
87 87 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
88 88 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
89 89 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
90 90 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
91 91 ])
92 92 def test_hooks_propagate(func, extension, event, hook_extras):
93 93 """
94 94 Tests that our hook code propagates to rhodecode extensions and triggers
95 95 the appropriate event.
96 96 """
97 97 class ExtensionMock(mock.Mock):
98 98 @property
99 99 def output(self):
100 100 return 'MOCK'
101 101
102 102 extension_mock = ExtensionMock()
103 103 events_mock = mock.Mock()
104 104 patches = {
105 105 'Repository': mock.Mock(),
106 106 'events': events_mock,
107 107 extension: extension_mock,
108 108 }
109 109
110 110 # Clear shadow repo flag.
111 111 hook_extras.is_shadow_repo = False
112 112
113 113 # Execute hook function.
114 114 with mock.patch.multiple(hooks_base, **patches):
115 115 func(hook_extras)
116 116
117 117 # Assert that extensions are called and event was fired.
118 118 extension_mock.called_once()
119 119 assert_called_with_mock(events_mock.trigger, event)
120 120
121 121
122 122 @pytest.mark.parametrize('func, extension, event', [
123 123 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
124 124 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
125 125 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
126 126 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
127 127 ])
128 128 def test_hooks_propagates_not_on_shadow(func, extension, event, hook_extras):
129 129 """
130 130 If hooks are called by a request to a shadow repo we only want to run our
131 131 internal hooks code but not external ones like rhodecode extensions or
132 132 trigger an event.
133 133 """
134 134 extension_mock = mock.Mock()
135 135 events_mock = mock.Mock()
136 136 patches = {
137 137 'Repository': mock.Mock(),
138 138 'events': events_mock,
139 139 extension: extension_mock,
140 140 }
141 141
142 142 # Set shadow repo flag.
143 143 hook_extras.is_shadow_repo = True
144 144
145 145 # Execute hook function.
146 146 with mock.patch.multiple(hooks_base, **patches):
147 147 func(hook_extras)
148 148
149 149 # Assert that extensions are *not* called and event was *not* fired.
150 150 assert not extension_mock.called
151 151 assert not events_mock.trigger.called
152
General Comments 0
You need to be logged in to leave comments. Login now