##// END OF EJS Templates
audit-logs: add push/pull actions to audit logs.
marcink -
r1736:70904f54 default
parent child Browse files
Show More
@@ -1,148 +1,165 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23
23
24 from rhodecode.model import meta
24 from rhodecode.model import meta
25 from rhodecode.model.db import User, UserLog
25 from rhodecode.model.db import User, UserLog, Repository
26
26
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30
30
31 ACTIONS = {
31 ACTIONS = {
32 'user.login.success': {},
32 'user.login.success': {},
33 'user.login.failure': {},
33 'user.login.failure': {},
34 'user.logout': {},
34 'user.logout': {},
35 'user.password.reset_request': {},
35 'user.password.reset_request': {},
36 'user.push': {},
37 'user.pull': {},
36
38
37 'repo.add': {},
39 'repo.add': {},
38 'repo.edit': {},
40 'repo.edit': {},
39 'repo.edit.permissions': {},
41 'repo.edit.permissions': {},
40 'repo.commit.strip': {}
42 'repo.commit.strip': {}
41 }
43 }
42
44
43
45
44 class UserWrap(object):
46 class UserWrap(object):
45 """
47 """
46 Fake object used to imitate AuthUser
48 Fake object used to imitate AuthUser
47 """
49 """
48
50
49 def __init__(self, user_id=None, username=None, ip_addr=None):
51 def __init__(self, user_id=None, username=None, ip_addr=None):
50 self.user_id = user_id
52 self.user_id = user_id
51 self.username = username
53 self.username = username
52 self.ip_addr = ip_addr
54 self.ip_addr = ip_addr
53
55
54
56
57 class RepoWrap(object):
58 """
59 Fake object used to imitate RepoObject that audit logger requires
60 """
61
62 def __init__(self, repo_id=None, repo_name=None):
63 self.repo_id = repo_id
64 self.repo_name = repo_name
65
66
55 def _store_log(action_name, action_data, user_id, username, user_data,
67 def _store_log(action_name, action_data, user_id, username, user_data,
56 ip_address, repository_id, repository_name):
68 ip_address, repository_id, repository_name):
57 user_log = UserLog()
69 user_log = UserLog()
58 user_log.version = UserLog.VERSION_2
70 user_log.version = UserLog.VERSION_2
59
71
60 user_log.action = action_name
72 user_log.action = action_name
61 user_log.action_data = action_data
73 user_log.action_data = action_data
62
74
63 user_log.user_ip = ip_address
75 user_log.user_ip = ip_address
64
76
65 user_log.user_id = user_id
77 user_log.user_id = user_id
66 user_log.username = username
78 user_log.username = username
67 user_log.user_data = user_data
79 user_log.user_data = user_data
68
80
69 user_log.repository_id = repository_id
81 user_log.repository_id = repository_id
70 user_log.repository_name = repository_name
82 user_log.repository_name = repository_name
71
83
72 user_log.action_date = datetime.datetime.now()
84 user_log.action_date = datetime.datetime.now()
73
85
74 log.info('AUDIT: Logging action: `%s` by user:id:%s[%s] ip:%s',
86 log.info('AUDIT: Logging action: `%s` by user:id:%s[%s] ip:%s',
75 action_name, user_id, username, ip_address)
87 action_name, user_id, username, ip_address)
76
88
77 return user_log
89 return user_log
78
90
79
91
80 def store(
92 def store(
81 action, user, action_data=None, user_data=None, ip_addr=None,
93 action, user, action_data=None, user_data=None, ip_addr=None,
82 repo=None, sa_session=None, commit=False):
94 repo=None, sa_session=None, commit=False):
83 """
95 """
84 Audit logger for various actions made by users, typically this results in a call such::
96 Audit logger for various actions made by users, typically this results in a call such::
85
97
86 from rhodecode.lib import audit_logger
98 from rhodecode.lib import audit_logger
87
99
88 audit_logger.store(action='repo.edit', user=self._rhodecode_user)
100 audit_logger.store(action='repo.edit', user=self._rhodecode_user)
89 audit_logger.store(action='repo.delete', user=audit_logger.UserWrap(username='itried-to-login', ip_addr='8.8.8.8'))
101 audit_logger.store(action='repo.delete', user=audit_logger.UserWrap(username='itried-to-login', ip_addr='8.8.8.8'))
90
102
91 # without an user ?
103 # without an user ?
92 audit_user = audit_logger.UserWrap(
104 audit_user = audit_logger.UserWrap(
93 username=self.request.params.get('username'),
105 username=self.request.params.get('username'),
94 ip_addr=self.request.remote_addr)
106 ip_addr=self.request.remote_addr)
95 audit_logger.store(action='user.login.failure', user=audit_user)
107 audit_logger.store(action='user.login.failure', user=audit_user)
96 """
108 """
97 from rhodecode.lib.utils2 import safe_unicode
109 from rhodecode.lib.utils2 import safe_unicode
98 from rhodecode.lib.auth import AuthUser
110 from rhodecode.lib.auth import AuthUser
99
111
100 if action not in ACTIONS:
112 if action not in ACTIONS:
101 raise ValueError('Action `{}` not in valid actions'.format(action))
113 raise ValueError('Action `{}` not in valid actions'.format(action))
102
114
103 if not sa_session:
115 if not sa_session:
104 sa_session = meta.Session()
116 sa_session = meta.Session()
105
117
106 try:
118 try:
107 username = getattr(user, 'username', None)
119 username = getattr(user, 'username', None)
108 if not username:
120 if not username:
109 pass
121 pass
110
122
111 user_id = getattr(user, 'user_id', None)
123 user_id = getattr(user, 'user_id', None)
112 if not user_id:
124 if not user_id:
113 # maybe we have username ? Try to figure user_id from username
125 # maybe we have username ? Try to figure user_id from username
114 if username:
126 if username:
115 user_id = getattr(
127 user_id = getattr(
116 User.get_by_username(username), 'user_id', None)
128 User.get_by_username(username), 'user_id', None)
117
129
118 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
130 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
119 if not ip_addr:
131 if not ip_addr:
120 pass
132 pass
121
133
122 if not user_data:
134 if not user_data:
123 # try to get this from the auth user
135 # try to get this from the auth user
124 if isinstance(user, AuthUser):
136 if isinstance(user, AuthUser):
125 user_data = {
137 user_data = {
126 'username': user.username,
138 'username': user.username,
127 'email': user.email,
139 'email': user.email,
128 }
140 }
129
141
142 repository_name = getattr(repo, 'repo_name', None)
130 repository_id = getattr(repo, 'repo_id', None)
143 repository_id = getattr(repo, 'repo_id', None)
131 repository_name = getattr(repo, 'repo_name', None)
144 if not repository_id:
145 # maybe we have repo_name ? Try to figure repo_id from repo_name
146 if repository_name:
147 repository_id = getattr(
148 Repository.get_by_repo_name(repository_name), 'repo_id', None)
132
149
133 user_log = _store_log(
150 user_log = _store_log(
134 action_name=safe_unicode(action),
151 action_name=safe_unicode(action),
135 action_data=action_data or {},
152 action_data=action_data or {},
136 user_id=user_id,
153 user_id=user_id,
137 username=username,
154 username=username,
138 user_data=user_data or {},
155 user_data=user_data or {},
139 ip_address=safe_unicode(ip_addr),
156 ip_address=safe_unicode(ip_addr),
140 repository_id=repository_id,
157 repository_id=repository_id,
141 repository_name=repository_name
158 repository_name=repository_name
142 )
159 )
143 sa_session.add(user_log)
160 sa_session.add(user_log)
144 if commit:
161 if commit:
145 sa_session.commit()
162 sa_session.commit()
146
163
147 except Exception:
164 except Exception:
148 log.exception('AUDIT: failed to store audit log')
165 log.exception('AUDIT: failed to store audit log')
@@ -1,401 +1,420 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Set of hooks run by RhodeCode Enterprise
23 Set of hooks run by RhodeCode Enterprise
24 """
24 """
25
25
26 import os
26 import os
27 import collections
27 import collections
28 import logging
28 import logging
29
29
30 import rhodecode
30 import rhodecode
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
33 from rhodecode.lib.utils import action_logger
34 from rhodecode.lib.utils import action_logger
34 from rhodecode.lib.utils2 import safe_str
35 from rhodecode.lib.utils2 import safe_str
35 from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
36 from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
36 from rhodecode.model.db import Repository, User
37 from rhodecode.model.db import Repository, User
37
38
38 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
39
40
40
41
41 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
42 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
42
43
43
44
44 def is_shadow_repo(extras):
45 def is_shadow_repo(extras):
45 """
46 """
46 Returns ``True`` if this is an action executed against a shadow repository.
47 Returns ``True`` if this is an action executed against a shadow repository.
47 """
48 """
48 return extras['is_shadow_repo']
49 return extras['is_shadow_repo']
49
50
50
51
51 def _get_scm_size(alias, root_path):
52 def _get_scm_size(alias, root_path):
52
53
53 if not alias.startswith('.'):
54 if not alias.startswith('.'):
54 alias += '.'
55 alias += '.'
55
56
56 size_scm, size_root = 0, 0
57 size_scm, size_root = 0, 0
57 for path, unused_dirs, files in os.walk(safe_str(root_path)):
58 for path, unused_dirs, files in os.walk(safe_str(root_path)):
58 if path.find(alias) != -1:
59 if path.find(alias) != -1:
59 for f in files:
60 for f in files:
60 try:
61 try:
61 size_scm += os.path.getsize(os.path.join(path, f))
62 size_scm += os.path.getsize(os.path.join(path, f))
62 except OSError:
63 except OSError:
63 pass
64 pass
64 else:
65 else:
65 for f in files:
66 for f in files:
66 try:
67 try:
67 size_root += os.path.getsize(os.path.join(path, f))
68 size_root += os.path.getsize(os.path.join(path, f))
68 except OSError:
69 except OSError:
69 pass
70 pass
70
71
71 size_scm_f = h.format_byte_size_binary(size_scm)
72 size_scm_f = h.format_byte_size_binary(size_scm)
72 size_root_f = h.format_byte_size_binary(size_root)
73 size_root_f = h.format_byte_size_binary(size_root)
73 size_total_f = h.format_byte_size_binary(size_root + size_scm)
74 size_total_f = h.format_byte_size_binary(size_root + size_scm)
74
75
75 return size_scm_f, size_root_f, size_total_f
76 return size_scm_f, size_root_f, size_total_f
76
77
77
78
78 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
79 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
79 def repo_size(extras):
80 def repo_size(extras):
80 """Present size of repository after push."""
81 """Present size of repository after push."""
81 repo = Repository.get_by_repo_name(extras.repository)
82 repo = Repository.get_by_repo_name(extras.repository)
82 vcs_part = safe_str(u'.%s' % repo.repo_type)
83 vcs_part = safe_str(u'.%s' % repo.repo_type)
83 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
84 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
84 repo.repo_full_path)
85 repo.repo_full_path)
85 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
86 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
86 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
87 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
87 return HookResponse(0, msg)
88 return HookResponse(0, msg)
88
89
89
90
90 def pre_push(extras):
91 def pre_push(extras):
91 """
92 """
92 Hook executed before pushing code.
93 Hook executed before pushing code.
93
94
94 It bans pushing when the repository is locked.
95 It bans pushing when the repository is locked.
95 """
96 """
96
97
97 usr = User.get_by_username(extras.username)
98 usr = User.get_by_username(extras.username)
98 output = ''
99 output = ''
99 if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
100 if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
100 locked_by = User.get(extras.locked_by[0]).username
101 locked_by = User.get(extras.locked_by[0]).username
101 reason = extras.locked_by[2]
102 reason = extras.locked_by[2]
102 # this exception is interpreted in git/hg middlewares and based
103 # this exception is interpreted in git/hg middlewares and based
103 # on that proper return code is server to client
104 # on that proper return code is server to client
104 _http_ret = HTTPLockedRC(
105 _http_ret = HTTPLockedRC(
105 _locked_by_explanation(extras.repository, locked_by, reason))
106 _locked_by_explanation(extras.repository, locked_by, reason))
106 if str(_http_ret.code).startswith('2'):
107 if str(_http_ret.code).startswith('2'):
107 # 2xx Codes don't raise exceptions
108 # 2xx Codes don't raise exceptions
108 output = _http_ret.title
109 output = _http_ret.title
109 else:
110 else:
110 raise _http_ret
111 raise _http_ret
111
112
112 # Propagate to external components. This is done after checking the
113 # Propagate to external components. This is done after checking the
113 # lock, for consistent behavior.
114 # lock, for consistent behavior.
114 if not is_shadow_repo(extras):
115 if not is_shadow_repo(extras):
115 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
116 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
116 events.trigger(events.RepoPrePushEvent(
117 events.trigger(events.RepoPrePushEvent(
117 repo_name=extras.repository, extras=extras))
118 repo_name=extras.repository, extras=extras))
118
119
119 return HookResponse(0, output)
120 return HookResponse(0, output)
120
121
121
122
122 def pre_pull(extras):
123 def pre_pull(extras):
123 """
124 """
124 Hook executed before pulling the code.
125 Hook executed before pulling the code.
125
126
126 It bans pulling when the repository is locked.
127 It bans pulling when the repository is locked.
127 """
128 """
128
129
129 output = ''
130 output = ''
130 if extras.locked_by[0]:
131 if extras.locked_by[0]:
131 locked_by = User.get(extras.locked_by[0]).username
132 locked_by = User.get(extras.locked_by[0]).username
132 reason = extras.locked_by[2]
133 reason = extras.locked_by[2]
133 # this exception is interpreted in git/hg middlewares and based
134 # this exception is interpreted in git/hg middlewares and based
134 # on that proper return code is server to client
135 # on that proper return code is server to client
135 _http_ret = HTTPLockedRC(
136 _http_ret = HTTPLockedRC(
136 _locked_by_explanation(extras.repository, locked_by, reason))
137 _locked_by_explanation(extras.repository, locked_by, reason))
137 if str(_http_ret.code).startswith('2'):
138 if str(_http_ret.code).startswith('2'):
138 # 2xx Codes don't raise exceptions
139 # 2xx Codes don't raise exceptions
139 output = _http_ret.title
140 output = _http_ret.title
140 else:
141 else:
141 raise _http_ret
142 raise _http_ret
142
143
143 # Propagate to external components. This is done after checking the
144 # Propagate to external components. This is done after checking the
144 # lock, for consistent behavior.
145 # lock, for consistent behavior.
145 if not is_shadow_repo(extras):
146 if not is_shadow_repo(extras):
146 pre_pull_extension(**extras)
147 pre_pull_extension(**extras)
147 events.trigger(events.RepoPrePullEvent(
148 events.trigger(events.RepoPrePullEvent(
148 repo_name=extras.repository, extras=extras))
149 repo_name=extras.repository, extras=extras))
149
150
150 return HookResponse(0, output)
151 return HookResponse(0, output)
151
152
152
153
153 def post_pull(extras):
154 def post_pull(extras):
154 """Hook executed after client pulls the code."""
155 """Hook executed after client pulls the code."""
155 user = User.get_by_username(extras.username)
156 user = User.get_by_username(extras.username)
156 action = 'pull'
157 action = 'pull'
157 action_logger(user, action, extras.repository, extras.ip, commit=True)
158 action_logger(user, action, extras.repository, extras.ip, commit=True)
158
159
160 audit_user = audit_logger.UserWrap(
161 username=extras.username,
162 ip_addr=extras.ip)
163 audit_logger.store(
164 action='user.pull', action_data={
165 'user_agent': extras.user_agent},
166 user=audit_user, commit=True)
167
159 # Propagate to external components.
168 # Propagate to external components.
160 if not is_shadow_repo(extras):
169 if not is_shadow_repo(extras):
161 post_pull_extension(**extras)
170 post_pull_extension(**extras)
162 events.trigger(events.RepoPullEvent(
171 events.trigger(events.RepoPullEvent(
163 repo_name=extras.repository, extras=extras))
172 repo_name=extras.repository, extras=extras))
164
173
165 output = ''
174 output = ''
166 # make lock is a tri state False, True, None. We only make lock on True
175 # make lock is a tri state False, True, None. We only make lock on True
167 if extras.make_lock is True and not is_shadow_repo(extras):
176 if extras.make_lock is True and not is_shadow_repo(extras):
168 Repository.lock(Repository.get_by_repo_name(extras.repository),
177 Repository.lock(Repository.get_by_repo_name(extras.repository),
169 user.user_id,
178 user.user_id,
170 lock_reason=Repository.LOCK_PULL)
179 lock_reason=Repository.LOCK_PULL)
171 msg = 'Made lock on repo `%s`' % (extras.repository,)
180 msg = 'Made lock on repo `%s`' % (extras.repository,)
172 output += msg
181 output += msg
173
182
174 if extras.locked_by[0]:
183 if extras.locked_by[0]:
175 locked_by = User.get(extras.locked_by[0]).username
184 locked_by = User.get(extras.locked_by[0]).username
176 reason = extras.locked_by[2]
185 reason = extras.locked_by[2]
177 _http_ret = HTTPLockedRC(
186 _http_ret = HTTPLockedRC(
178 _locked_by_explanation(extras.repository, locked_by, reason))
187 _locked_by_explanation(extras.repository, locked_by, reason))
179 if str(_http_ret.code).startswith('2'):
188 if str(_http_ret.code).startswith('2'):
180 # 2xx Codes don't raise exceptions
189 # 2xx Codes don't raise exceptions
181 output += _http_ret.title
190 output += _http_ret.title
182
191
183 return HookResponse(0, output)
192 return HookResponse(0, output)
184
193
185
194
186 def post_push(extras):
195 def post_push(extras):
187 """Hook executed after user pushes to the repository."""
196 """Hook executed after user pushes to the repository."""
188 action_tmpl = extras.action + ':%s'
197 action_tmpl = extras.action + ':%s'
189 commit_ids = extras.commit_ids[:29000]
198 commit_ids = extras.commit_ids[:29000]
190
199
191 action = action_tmpl % ','.join(commit_ids)
200 action = action_tmpl % ','.join(commit_ids)
192 action_logger(
201 action_logger(
193 extras.username, action, extras.repository, extras.ip, commit=True)
202 extras.username, action, extras.repository, extras.ip, commit=True)
194
203
204 audit_user = audit_logger.UserWrap(
205 username=extras.username,
206 ip_addr=extras.ip)
207 repo = audit_logger.RepoWrap(repo_name=extras.repository)
208 audit_logger.store(
209 action='user.push', action_data={
210 'user_agent': extras.user_agent,
211 'commit_ids': commit_ids[:10000]},
212 user=audit_user, repo=repo, commit=True)
213
195 # Propagate to external components.
214 # Propagate to external components.
196 if not is_shadow_repo(extras):
215 if not is_shadow_repo(extras):
197 post_push_extension(
216 post_push_extension(
198 repo_store_path=Repository.base_path(),
217 repo_store_path=Repository.base_path(),
199 pushed_revs=commit_ids,
218 pushed_revs=commit_ids,
200 **extras)
219 **extras)
201 events.trigger(events.RepoPushEvent(
220 events.trigger(events.RepoPushEvent(
202 repo_name=extras.repository,
221 repo_name=extras.repository,
203 pushed_commit_ids=commit_ids,
222 pushed_commit_ids=commit_ids,
204 extras=extras))
223 extras=extras))
205
224
206 output = ''
225 output = ''
207 # make lock is a tri state False, True, None. We only release lock on False
226 # make lock is a tri state False, True, None. We only release lock on False
208 if extras.make_lock is False and not is_shadow_repo(extras):
227 if extras.make_lock is False and not is_shadow_repo(extras):
209 Repository.unlock(Repository.get_by_repo_name(extras.repository))
228 Repository.unlock(Repository.get_by_repo_name(extras.repository))
210 msg = 'Released lock on repo `%s`\n' % extras.repository
229 msg = 'Released lock on repo `%s`\n' % extras.repository
211 output += msg
230 output += msg
212
231
213 if extras.locked_by[0]:
232 if extras.locked_by[0]:
214 locked_by = User.get(extras.locked_by[0]).username
233 locked_by = User.get(extras.locked_by[0]).username
215 reason = extras.locked_by[2]
234 reason = extras.locked_by[2]
216 _http_ret = HTTPLockedRC(
235 _http_ret = HTTPLockedRC(
217 _locked_by_explanation(extras.repository, locked_by, reason))
236 _locked_by_explanation(extras.repository, locked_by, reason))
218 # TODO: johbo: if not?
237 # TODO: johbo: if not?
219 if str(_http_ret.code).startswith('2'):
238 if str(_http_ret.code).startswith('2'):
220 # 2xx Codes don't raise exceptions
239 # 2xx Codes don't raise exceptions
221 output += _http_ret.title
240 output += _http_ret.title
222
241
223 output += 'RhodeCode: push completed\n'
242 output += 'RhodeCode: push completed\n'
224
243
225 return HookResponse(0, output)
244 return HookResponse(0, output)
226
245
227
246
228 def _locked_by_explanation(repo_name, user_name, reason):
247 def _locked_by_explanation(repo_name, user_name, reason):
229 message = (
248 message = (
230 'Repository `%s` locked by user `%s`. Reason:`%s`'
249 'Repository `%s` locked by user `%s`. Reason:`%s`'
231 % (repo_name, user_name, reason))
250 % (repo_name, user_name, reason))
232 return message
251 return message
233
252
234
253
235 def check_allowed_create_user(user_dict, created_by, **kwargs):
254 def check_allowed_create_user(user_dict, created_by, **kwargs):
236 # pre create hooks
255 # pre create hooks
237 if pre_create_user.is_active():
256 if pre_create_user.is_active():
238 allowed, reason = pre_create_user(created_by=created_by, **user_dict)
257 allowed, reason = pre_create_user(created_by=created_by, **user_dict)
239 if not allowed:
258 if not allowed:
240 raise UserCreationError(reason)
259 raise UserCreationError(reason)
241
260
242
261
243 class ExtensionCallback(object):
262 class ExtensionCallback(object):
244 """
263 """
245 Forwards a given call to rcextensions, sanitizes keyword arguments.
264 Forwards a given call to rcextensions, sanitizes keyword arguments.
246
265
247 Does check if there is an extension active for that hook. If it is
266 Does check if there is an extension active for that hook. If it is
248 there, it will forward all `kwargs_keys` keyword arguments to the
267 there, it will forward all `kwargs_keys` keyword arguments to the
249 extension callback.
268 extension callback.
250 """
269 """
251
270
252 def __init__(self, hook_name, kwargs_keys):
271 def __init__(self, hook_name, kwargs_keys):
253 self._hook_name = hook_name
272 self._hook_name = hook_name
254 self._kwargs_keys = set(kwargs_keys)
273 self._kwargs_keys = set(kwargs_keys)
255
274
256 def __call__(self, *args, **kwargs):
275 def __call__(self, *args, **kwargs):
257 log.debug('Calling extension callback for %s', self._hook_name)
276 log.debug('Calling extension callback for %s', self._hook_name)
258
277
259 kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
278 kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
260 # backward compat for removed api_key for old hooks. THis was it works
279 # backward compat for removed api_key for old hooks. THis was it works
261 # with older rcextensions that require api_key present
280 # with older rcextensions that require api_key present
262 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
281 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
263 kwargs_to_pass['api_key'] = '_DEPRECATED_'
282 kwargs_to_pass['api_key'] = '_DEPRECATED_'
264
283
265 callback = self._get_callback()
284 callback = self._get_callback()
266 if callback:
285 if callback:
267 return callback(**kwargs_to_pass)
286 return callback(**kwargs_to_pass)
268 else:
287 else:
269 log.debug('extensions callback not found skipping...')
288 log.debug('extensions callback not found skipping...')
270
289
271 def is_active(self):
290 def is_active(self):
272 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
291 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
273
292
274 def _get_callback(self):
293 def _get_callback(self):
275 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
294 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
276
295
277
296
278 pre_pull_extension = ExtensionCallback(
297 pre_pull_extension = ExtensionCallback(
279 hook_name='PRE_PULL_HOOK',
298 hook_name='PRE_PULL_HOOK',
280 kwargs_keys=(
299 kwargs_keys=(
281 'server_url', 'config', 'scm', 'username', 'ip', 'action',
300 'server_url', 'config', 'scm', 'username', 'ip', 'action',
282 'repository'))
301 'repository'))
283
302
284
303
285 post_pull_extension = ExtensionCallback(
304 post_pull_extension = ExtensionCallback(
286 hook_name='PULL_HOOK',
305 hook_name='PULL_HOOK',
287 kwargs_keys=(
306 kwargs_keys=(
288 'server_url', 'config', 'scm', 'username', 'ip', 'action',
307 'server_url', 'config', 'scm', 'username', 'ip', 'action',
289 'repository'))
308 'repository'))
290
309
291
310
292 pre_push_extension = ExtensionCallback(
311 pre_push_extension = ExtensionCallback(
293 hook_name='PRE_PUSH_HOOK',
312 hook_name='PRE_PUSH_HOOK',
294 kwargs_keys=(
313 kwargs_keys=(
295 'server_url', 'config', 'scm', 'username', 'ip', 'action',
314 'server_url', 'config', 'scm', 'username', 'ip', 'action',
296 'repository', 'repo_store_path', 'commit_ids'))
315 'repository', 'repo_store_path', 'commit_ids'))
297
316
298
317
299 post_push_extension = ExtensionCallback(
318 post_push_extension = ExtensionCallback(
300 hook_name='PUSH_HOOK',
319 hook_name='PUSH_HOOK',
301 kwargs_keys=(
320 kwargs_keys=(
302 'server_url', 'config', 'scm', 'username', 'ip', 'action',
321 'server_url', 'config', 'scm', 'username', 'ip', 'action',
303 'repository', 'repo_store_path', 'pushed_revs'))
322 'repository', 'repo_store_path', 'pushed_revs'))
304
323
305
324
306 pre_create_user = ExtensionCallback(
325 pre_create_user = ExtensionCallback(
307 hook_name='PRE_CREATE_USER_HOOK',
326 hook_name='PRE_CREATE_USER_HOOK',
308 kwargs_keys=(
327 kwargs_keys=(
309 'username', 'password', 'email', 'firstname', 'lastname', 'active',
328 'username', 'password', 'email', 'firstname', 'lastname', 'active',
310 'admin', 'created_by'))
329 'admin', 'created_by'))
311
330
312
331
313 log_create_pull_request = ExtensionCallback(
332 log_create_pull_request = ExtensionCallback(
314 hook_name='CREATE_PULL_REQUEST',
333 hook_name='CREATE_PULL_REQUEST',
315 kwargs_keys=(
334 kwargs_keys=(
316 'server_url', 'config', 'scm', 'username', 'ip', 'action',
335 'server_url', 'config', 'scm', 'username', 'ip', 'action',
317 'repository', 'pull_request_id', 'url', 'title', 'description',
336 'repository', 'pull_request_id', 'url', 'title', 'description',
318 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
337 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
319 'mergeable', 'source', 'target', 'author', 'reviewers'))
338 'mergeable', 'source', 'target', 'author', 'reviewers'))
320
339
321
340
322 log_merge_pull_request = ExtensionCallback(
341 log_merge_pull_request = ExtensionCallback(
323 hook_name='MERGE_PULL_REQUEST',
342 hook_name='MERGE_PULL_REQUEST',
324 kwargs_keys=(
343 kwargs_keys=(
325 'server_url', 'config', 'scm', 'username', 'ip', 'action',
344 'server_url', 'config', 'scm', 'username', 'ip', 'action',
326 'repository', 'pull_request_id', 'url', 'title', 'description',
345 'repository', 'pull_request_id', 'url', 'title', 'description',
327 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
346 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
328 'mergeable', 'source', 'target', 'author', 'reviewers'))
347 'mergeable', 'source', 'target', 'author', 'reviewers'))
329
348
330
349
331 log_close_pull_request = ExtensionCallback(
350 log_close_pull_request = ExtensionCallback(
332 hook_name='CLOSE_PULL_REQUEST',
351 hook_name='CLOSE_PULL_REQUEST',
333 kwargs_keys=(
352 kwargs_keys=(
334 'server_url', 'config', 'scm', 'username', 'ip', 'action',
353 'server_url', 'config', 'scm', 'username', 'ip', 'action',
335 'repository', 'pull_request_id', 'url', 'title', 'description',
354 'repository', 'pull_request_id', 'url', 'title', 'description',
336 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
355 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
337 'mergeable', 'source', 'target', 'author', 'reviewers'))
356 'mergeable', 'source', 'target', 'author', 'reviewers'))
338
357
339
358
340 log_review_pull_request = ExtensionCallback(
359 log_review_pull_request = ExtensionCallback(
341 hook_name='REVIEW_PULL_REQUEST',
360 hook_name='REVIEW_PULL_REQUEST',
342 kwargs_keys=(
361 kwargs_keys=(
343 'server_url', 'config', 'scm', 'username', 'ip', 'action',
362 'server_url', 'config', 'scm', 'username', 'ip', 'action',
344 'repository', 'pull_request_id', 'url', 'title', 'description',
363 'repository', 'pull_request_id', 'url', 'title', 'description',
345 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
364 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
346 'mergeable', 'source', 'target', 'author', 'reviewers'))
365 'mergeable', 'source', 'target', 'author', 'reviewers'))
347
366
348
367
349 log_update_pull_request = ExtensionCallback(
368 log_update_pull_request = ExtensionCallback(
350 hook_name='UPDATE_PULL_REQUEST',
369 hook_name='UPDATE_PULL_REQUEST',
351 kwargs_keys=(
370 kwargs_keys=(
352 'server_url', 'config', 'scm', 'username', 'ip', 'action',
371 'server_url', 'config', 'scm', 'username', 'ip', 'action',
353 'repository', 'pull_request_id', 'url', 'title', 'description',
372 'repository', 'pull_request_id', 'url', 'title', 'description',
354 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
373 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
355 'mergeable', 'source', 'target', 'author', 'reviewers'))
374 'mergeable', 'source', 'target', 'author', 'reviewers'))
356
375
357
376
358 log_create_user = ExtensionCallback(
377 log_create_user = ExtensionCallback(
359 hook_name='CREATE_USER_HOOK',
378 hook_name='CREATE_USER_HOOK',
360 kwargs_keys=(
379 kwargs_keys=(
361 'username', 'full_name_or_username', 'full_contact', 'user_id',
380 'username', 'full_name_or_username', 'full_contact', 'user_id',
362 'name', 'firstname', 'short_contact', 'admin', 'lastname',
381 'name', 'firstname', 'short_contact', 'admin', 'lastname',
363 'ip_addresses', 'extern_type', 'extern_name',
382 'ip_addresses', 'extern_type', 'extern_name',
364 'email', 'api_keys', 'last_login',
383 'email', 'api_keys', 'last_login',
365 'full_name', 'active', 'password', 'emails',
384 'full_name', 'active', 'password', 'emails',
366 'inherit_default_permissions', 'created_by', 'created_on'))
385 'inherit_default_permissions', 'created_by', 'created_on'))
367
386
368
387
369 log_delete_user = ExtensionCallback(
388 log_delete_user = ExtensionCallback(
370 hook_name='DELETE_USER_HOOK',
389 hook_name='DELETE_USER_HOOK',
371 kwargs_keys=(
390 kwargs_keys=(
372 'username', 'full_name_or_username', 'full_contact', 'user_id',
391 'username', 'full_name_or_username', 'full_contact', 'user_id',
373 'name', 'firstname', 'short_contact', 'admin', 'lastname',
392 'name', 'firstname', 'short_contact', 'admin', 'lastname',
374 'ip_addresses',
393 'ip_addresses',
375 'email', 'last_login',
394 'email', 'last_login',
376 'full_name', 'active', 'password', 'emails',
395 'full_name', 'active', 'password', 'emails',
377 'inherit_default_permissions', 'deleted_by'))
396 'inherit_default_permissions', 'deleted_by'))
378
397
379
398
380 log_create_repository = ExtensionCallback(
399 log_create_repository = ExtensionCallback(
381 hook_name='CREATE_REPO_HOOK',
400 hook_name='CREATE_REPO_HOOK',
382 kwargs_keys=(
401 kwargs_keys=(
383 'repo_name', 'repo_type', 'description', 'private', 'created_on',
402 'repo_name', 'repo_type', 'description', 'private', 'created_on',
384 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
403 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
385 'clone_uri', 'fork_id', 'group_id', 'created_by'))
404 'clone_uri', 'fork_id', 'group_id', 'created_by'))
386
405
387
406
388 log_delete_repository = ExtensionCallback(
407 log_delete_repository = ExtensionCallback(
389 hook_name='DELETE_REPO_HOOK',
408 hook_name='DELETE_REPO_HOOK',
390 kwargs_keys=(
409 kwargs_keys=(
391 'repo_name', 'repo_type', 'description', 'private', 'created_on',
410 'repo_name', 'repo_type', 'description', 'private', 'created_on',
392 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
411 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
393 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
412 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
394
413
395
414
396 log_create_repository_group = ExtensionCallback(
415 log_create_repository_group = ExtensionCallback(
397 hook_name='CREATE_REPO_GROUP_HOOK',
416 hook_name='CREATE_REPO_GROUP_HOOK',
398 kwargs_keys=(
417 kwargs_keys=(
399 'group_name', 'group_parent_id', 'group_description',
418 'group_name', 'group_parent_id', 'group_description',
400 'group_id', 'user_id', 'created_by', 'created_on',
419 'group_id', 'user_id', 'created_by', 'created_on',
401 'enable_locking'))
420 'enable_locking'))
@@ -1,1095 +1,1097 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification)
29 PullRequest, ChangesetStatus, UserLog, Notification)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 from rhodecode.tests.utils import AssertResponse
36 from rhodecode.tests.utils import AssertResponse
37
37
38
38
39 @pytest.mark.usefixtures('app', 'autologin_user')
39 @pytest.mark.usefixtures('app', 'autologin_user')
40 @pytest.mark.backends("git", "hg")
40 @pytest.mark.backends("git", "hg")
41 class TestPullrequestsController:
41 class TestPullrequestsController:
42
42
43 def test_index(self, backend):
43 def test_index(self, backend):
44 self.app.get(url(
44 self.app.get(url(
45 controller='pullrequests', action='index',
45 controller='pullrequests', action='index',
46 repo_name=backend.repo_name))
46 repo_name=backend.repo_name))
47
47
48 def test_option_menu_create_pull_request_exists(self, backend):
48 def test_option_menu_create_pull_request_exists(self, backend):
49 repo_name = backend.repo_name
49 repo_name = backend.repo_name
50 response = self.app.get(url('summary_home', repo_name=repo_name))
50 response = self.app.get(url('summary_home', repo_name=repo_name))
51
51
52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
53 'pullrequest', repo_name=repo_name)
53 'pullrequest', repo_name=repo_name)
54 response.mustcontain(create_pr_link)
54 response.mustcontain(create_pr_link)
55
55
56 def test_global_redirect_of_pr(self, backend, pr_util):
56 def test_global_redirect_of_pr(self, backend, pr_util):
57 pull_request = pr_util.create_pull_request()
57 pull_request = pr_util.create_pull_request()
58
58
59 response = self.app.get(
59 response = self.app.get(
60 url('pull_requests_global',
60 url('pull_requests_global',
61 pull_request_id=pull_request.pull_request_id))
61 pull_request_id=pull_request.pull_request_id))
62
62
63 repo_name = pull_request.target_repo.repo_name
63 repo_name = pull_request.target_repo.repo_name
64 redirect_url = url('pullrequest_show', repo_name=repo_name,
64 redirect_url = url('pullrequest_show', repo_name=repo_name,
65 pull_request_id=pull_request.pull_request_id)
65 pull_request_id=pull_request.pull_request_id)
66 assert response.status == '302 Found'
66 assert response.status == '302 Found'
67 assert redirect_url in response.location
67 assert redirect_url in response.location
68
68
69 def test_create_pr_form_with_raw_commit_id(self, backend):
69 def test_create_pr_form_with_raw_commit_id(self, backend):
70 repo = backend.repo
70 repo = backend.repo
71
71
72 self.app.get(
72 self.app.get(
73 url(controller='pullrequests', action='index',
73 url(controller='pullrequests', action='index',
74 repo_name=repo.repo_name,
74 repo_name=repo.repo_name,
75 commit=repo.get_commit().raw_id),
75 commit=repo.get_commit().raw_id),
76 status=200)
76 status=200)
77
77
78 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
78 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
79 def test_show(self, pr_util, pr_merge_enabled):
79 def test_show(self, pr_util, pr_merge_enabled):
80 pull_request = pr_util.create_pull_request(
80 pull_request = pr_util.create_pull_request(
81 mergeable=pr_merge_enabled, enable_notifications=False)
81 mergeable=pr_merge_enabled, enable_notifications=False)
82
82
83 response = self.app.get(url(
83 response = self.app.get(url(
84 controller='pullrequests', action='show',
84 controller='pullrequests', action='show',
85 repo_name=pull_request.target_repo.scm_instance().name,
85 repo_name=pull_request.target_repo.scm_instance().name,
86 pull_request_id=str(pull_request.pull_request_id)))
86 pull_request_id=str(pull_request.pull_request_id)))
87
87
88 for commit_id in pull_request.revisions:
88 for commit_id in pull_request.revisions:
89 response.mustcontain(commit_id)
89 response.mustcontain(commit_id)
90
90
91 assert pull_request.target_ref_parts.type in response
91 assert pull_request.target_ref_parts.type in response
92 assert pull_request.target_ref_parts.name in response
92 assert pull_request.target_ref_parts.name in response
93 target_clone_url = pull_request.target_repo.clone_url()
93 target_clone_url = pull_request.target_repo.clone_url()
94 assert target_clone_url in response
94 assert target_clone_url in response
95
95
96 assert 'class="pull-request-merge"' in response
96 assert 'class="pull-request-merge"' in response
97 assert (
97 assert (
98 'Server-side pull request merging is disabled.'
98 'Server-side pull request merging is disabled.'
99 in response) != pr_merge_enabled
99 in response) != pr_merge_enabled
100
100
101 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
101 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
102 from rhodecode.tests.functional.test_login import login_url, logut_url
102 from rhodecode.tests.functional.test_login import login_url, logut_url
103 # Logout
103 # Logout
104 response = self.app.post(
104 response = self.app.post(
105 logut_url,
105 logut_url,
106 params={'csrf_token': csrf_token})
106 params={'csrf_token': csrf_token})
107 # Login as regular user
107 # Login as regular user
108 response = self.app.post(login_url,
108 response = self.app.post(login_url,
109 {'username': TEST_USER_REGULAR_LOGIN,
109 {'username': TEST_USER_REGULAR_LOGIN,
110 'password': 'test12'})
110 'password': 'test12'})
111
111
112 pull_request = pr_util.create_pull_request(
112 pull_request = pr_util.create_pull_request(
113 author=TEST_USER_REGULAR_LOGIN)
113 author=TEST_USER_REGULAR_LOGIN)
114
114
115 response = self.app.get(url(
115 response = self.app.get(url(
116 controller='pullrequests', action='show',
116 controller='pullrequests', action='show',
117 repo_name=pull_request.target_repo.scm_instance().name,
117 repo_name=pull_request.target_repo.scm_instance().name,
118 pull_request_id=str(pull_request.pull_request_id)))
118 pull_request_id=str(pull_request.pull_request_id)))
119
119
120 response.mustcontain('Server-side pull request merging is disabled.')
120 response.mustcontain('Server-side pull request merging is disabled.')
121
121
122 assert_response = response.assert_response()
122 assert_response = response.assert_response()
123 # for regular user without a merge permissions, we don't see it
123 # for regular user without a merge permissions, we don't see it
124 assert_response.no_element_exists('#close-pull-request-action')
124 assert_response.no_element_exists('#close-pull-request-action')
125
125
126 user_util.grant_user_permission_to_repo(
126 user_util.grant_user_permission_to_repo(
127 pull_request.target_repo,
127 pull_request.target_repo,
128 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
128 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
129 'repository.write')
129 'repository.write')
130 response = self.app.get(url(
130 response = self.app.get(url(
131 controller='pullrequests', action='show',
131 controller='pullrequests', action='show',
132 repo_name=pull_request.target_repo.scm_instance().name,
132 repo_name=pull_request.target_repo.scm_instance().name,
133 pull_request_id=str(pull_request.pull_request_id)))
133 pull_request_id=str(pull_request.pull_request_id)))
134
134
135 response.mustcontain('Server-side pull request merging is disabled.')
135 response.mustcontain('Server-side pull request merging is disabled.')
136
136
137 assert_response = response.assert_response()
137 assert_response = response.assert_response()
138 # now regular user has a merge permissions, we have CLOSE button
138 # now regular user has a merge permissions, we have CLOSE button
139 assert_response.one_element_exists('#close-pull-request-action')
139 assert_response.one_element_exists('#close-pull-request-action')
140
140
141 def test_show_invalid_commit_id(self, pr_util):
141 def test_show_invalid_commit_id(self, pr_util):
142 # Simulating invalid revisions which will cause a lookup error
142 # Simulating invalid revisions which will cause a lookup error
143 pull_request = pr_util.create_pull_request()
143 pull_request = pr_util.create_pull_request()
144 pull_request.revisions = ['invalid']
144 pull_request.revisions = ['invalid']
145 Session().add(pull_request)
145 Session().add(pull_request)
146 Session().commit()
146 Session().commit()
147
147
148 response = self.app.get(url(
148 response = self.app.get(url(
149 controller='pullrequests', action='show',
149 controller='pullrequests', action='show',
150 repo_name=pull_request.target_repo.scm_instance().name,
150 repo_name=pull_request.target_repo.scm_instance().name,
151 pull_request_id=str(pull_request.pull_request_id)))
151 pull_request_id=str(pull_request.pull_request_id)))
152
152
153 for commit_id in pull_request.revisions:
153 for commit_id in pull_request.revisions:
154 response.mustcontain(commit_id)
154 response.mustcontain(commit_id)
155
155
156 def test_show_invalid_source_reference(self, pr_util):
156 def test_show_invalid_source_reference(self, pr_util):
157 pull_request = pr_util.create_pull_request()
157 pull_request = pr_util.create_pull_request()
158 pull_request.source_ref = 'branch:b:invalid'
158 pull_request.source_ref = 'branch:b:invalid'
159 Session().add(pull_request)
159 Session().add(pull_request)
160 Session().commit()
160 Session().commit()
161
161
162 self.app.get(url(
162 self.app.get(url(
163 controller='pullrequests', action='show',
163 controller='pullrequests', action='show',
164 repo_name=pull_request.target_repo.scm_instance().name,
164 repo_name=pull_request.target_repo.scm_instance().name,
165 pull_request_id=str(pull_request.pull_request_id)))
165 pull_request_id=str(pull_request.pull_request_id)))
166
166
167 def test_edit_title_description(self, pr_util, csrf_token):
167 def test_edit_title_description(self, pr_util, csrf_token):
168 pull_request = pr_util.create_pull_request()
168 pull_request = pr_util.create_pull_request()
169 pull_request_id = pull_request.pull_request_id
169 pull_request_id = pull_request.pull_request_id
170
170
171 response = self.app.post(
171 response = self.app.post(
172 url(controller='pullrequests', action='update',
172 url(controller='pullrequests', action='update',
173 repo_name=pull_request.target_repo.repo_name,
173 repo_name=pull_request.target_repo.repo_name,
174 pull_request_id=str(pull_request_id)),
174 pull_request_id=str(pull_request_id)),
175 params={
175 params={
176 'edit_pull_request': 'true',
176 'edit_pull_request': 'true',
177 '_method': 'put',
177 '_method': 'put',
178 'title': 'New title',
178 'title': 'New title',
179 'description': 'New description',
179 'description': 'New description',
180 'csrf_token': csrf_token})
180 'csrf_token': csrf_token})
181
181
182 assert_session_flash(
182 assert_session_flash(
183 response, u'Pull request title & description updated.',
183 response, u'Pull request title & description updated.',
184 category='success')
184 category='success')
185
185
186 pull_request = PullRequest.get(pull_request_id)
186 pull_request = PullRequest.get(pull_request_id)
187 assert pull_request.title == 'New title'
187 assert pull_request.title == 'New title'
188 assert pull_request.description == 'New description'
188 assert pull_request.description == 'New description'
189
189
190 def test_edit_title_description_closed(self, pr_util, csrf_token):
190 def test_edit_title_description_closed(self, pr_util, csrf_token):
191 pull_request = pr_util.create_pull_request()
191 pull_request = pr_util.create_pull_request()
192 pull_request_id = pull_request.pull_request_id
192 pull_request_id = pull_request.pull_request_id
193 pr_util.close()
193 pr_util.close()
194
194
195 response = self.app.post(
195 response = self.app.post(
196 url(controller='pullrequests', action='update',
196 url(controller='pullrequests', action='update',
197 repo_name=pull_request.target_repo.repo_name,
197 repo_name=pull_request.target_repo.repo_name,
198 pull_request_id=str(pull_request_id)),
198 pull_request_id=str(pull_request_id)),
199 params={
199 params={
200 'edit_pull_request': 'true',
200 'edit_pull_request': 'true',
201 '_method': 'put',
201 '_method': 'put',
202 'title': 'New title',
202 'title': 'New title',
203 'description': 'New description',
203 'description': 'New description',
204 'csrf_token': csrf_token})
204 'csrf_token': csrf_token})
205
205
206 assert_session_flash(
206 assert_session_flash(
207 response, u'Cannot update closed pull requests.',
207 response, u'Cannot update closed pull requests.',
208 category='error')
208 category='error')
209
209
210 def test_update_invalid_source_reference(self, pr_util, csrf_token):
210 def test_update_invalid_source_reference(self, pr_util, csrf_token):
211 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
211 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
212
212
213 pull_request = pr_util.create_pull_request()
213 pull_request = pr_util.create_pull_request()
214 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
214 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
215 Session().add(pull_request)
215 Session().add(pull_request)
216 Session().commit()
216 Session().commit()
217
217
218 pull_request_id = pull_request.pull_request_id
218 pull_request_id = pull_request.pull_request_id
219
219
220 response = self.app.post(
220 response = self.app.post(
221 url(controller='pullrequests', action='update',
221 url(controller='pullrequests', action='update',
222 repo_name=pull_request.target_repo.repo_name,
222 repo_name=pull_request.target_repo.repo_name,
223 pull_request_id=str(pull_request_id)),
223 pull_request_id=str(pull_request_id)),
224 params={'update_commits': 'true', '_method': 'put',
224 params={'update_commits': 'true', '_method': 'put',
225 'csrf_token': csrf_token})
225 'csrf_token': csrf_token})
226
226
227 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
227 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
228 UpdateFailureReason.MISSING_SOURCE_REF]
228 UpdateFailureReason.MISSING_SOURCE_REF]
229 assert_session_flash(response, expected_msg, category='error')
229 assert_session_flash(response, expected_msg, category='error')
230
230
231 def test_missing_target_reference(self, pr_util, csrf_token):
231 def test_missing_target_reference(self, pr_util, csrf_token):
232 from rhodecode.lib.vcs.backends.base import MergeFailureReason
232 from rhodecode.lib.vcs.backends.base import MergeFailureReason
233 pull_request = pr_util.create_pull_request(
233 pull_request = pr_util.create_pull_request(
234 approved=True, mergeable=True)
234 approved=True, mergeable=True)
235 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
235 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
236 Session().add(pull_request)
236 Session().add(pull_request)
237 Session().commit()
237 Session().commit()
238
238
239 pull_request_id = pull_request.pull_request_id
239 pull_request_id = pull_request.pull_request_id
240 pull_request_url = url(
240 pull_request_url = url(
241 controller='pullrequests', action='show',
241 controller='pullrequests', action='show',
242 repo_name=pull_request.target_repo.repo_name,
242 repo_name=pull_request.target_repo.repo_name,
243 pull_request_id=str(pull_request_id))
243 pull_request_id=str(pull_request_id))
244
244
245 response = self.app.get(pull_request_url)
245 response = self.app.get(pull_request_url)
246
246
247 assertr = AssertResponse(response)
247 assertr = AssertResponse(response)
248 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
248 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
249 MergeFailureReason.MISSING_TARGET_REF]
249 MergeFailureReason.MISSING_TARGET_REF]
250 assertr.element_contains(
250 assertr.element_contains(
251 'span[data-role="merge-message"]', str(expected_msg))
251 'span[data-role="merge-message"]', str(expected_msg))
252
252
253 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
253 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
254 pull_request = pr_util.create_pull_request(approved=True)
254 pull_request = pr_util.create_pull_request(approved=True)
255 pull_request_id = pull_request.pull_request_id
255 pull_request_id = pull_request.pull_request_id
256 author = pull_request.user_id
256 author = pull_request.user_id
257 repo = pull_request.target_repo.repo_id
257 repo = pull_request.target_repo.repo_id
258
258
259 self.app.post(
259 self.app.post(
260 url(controller='pullrequests',
260 url(controller='pullrequests',
261 action='comment',
261 action='comment',
262 repo_name=pull_request.target_repo.scm_instance().name,
262 repo_name=pull_request.target_repo.scm_instance().name,
263 pull_request_id=str(pull_request_id)),
263 pull_request_id=str(pull_request_id)),
264 params={
264 params={
265 'changeset_status': ChangesetStatus.STATUS_APPROVED,
265 'changeset_status': ChangesetStatus.STATUS_APPROVED,
266 'close_pull_request': '1',
266 'close_pull_request': '1',
267 'text': 'Closing a PR',
267 'text': 'Closing a PR',
268 'csrf_token': csrf_token},
268 'csrf_token': csrf_token},
269 status=302)
269 status=302)
270
270
271 action = 'user_closed_pull_request:%d' % pull_request_id
271 action = 'user_closed_pull_request:%d' % pull_request_id
272 journal = UserLog.query()\
272 journal = UserLog.query()\
273 .filter(UserLog.user_id == author)\
273 .filter(UserLog.user_id == author)\
274 .filter(UserLog.repository_id == repo)\
274 .filter(UserLog.repository_id == repo)\
275 .filter(UserLog.action == action)\
275 .filter(UserLog.action == action)\
276 .all()
276 .all()
277 assert len(journal) == 1
277 assert len(journal) == 1
278
278
279 pull_request = PullRequest.get(pull_request_id)
279 pull_request = PullRequest.get(pull_request_id)
280 assert pull_request.is_closed()
280 assert pull_request.is_closed()
281
281
282 # check only the latest status, not the review status
282 # check only the latest status, not the review status
283 status = ChangesetStatusModel().get_status(
283 status = ChangesetStatusModel().get_status(
284 pull_request.source_repo, pull_request=pull_request)
284 pull_request.source_repo, pull_request=pull_request)
285 assert status == ChangesetStatus.STATUS_APPROVED
285 assert status == ChangesetStatus.STATUS_APPROVED
286
286
287 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
287 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
288 pull_request = pr_util.create_pull_request()
288 pull_request = pr_util.create_pull_request()
289 pull_request_id = pull_request.pull_request_id
289 pull_request_id = pull_request.pull_request_id
290 response = self.app.post(
290 response = self.app.post(
291 url(controller='pullrequests',
291 url(controller='pullrequests',
292 action='update',
292 action='update',
293 repo_name=pull_request.target_repo.scm_instance().name,
293 repo_name=pull_request.target_repo.scm_instance().name,
294 pull_request_id=str(pull_request.pull_request_id)),
294 pull_request_id=str(pull_request.pull_request_id)),
295 params={'close_pull_request': 'true', '_method': 'put',
295 params={'close_pull_request': 'true', '_method': 'put',
296 'csrf_token': csrf_token})
296 'csrf_token': csrf_token})
297
297
298 pull_request = PullRequest.get(pull_request_id)
298 pull_request = PullRequest.get(pull_request_id)
299
299
300 assert response.json is True
300 assert response.json is True
301 assert pull_request.is_closed()
301 assert pull_request.is_closed()
302
302
303 # check only the latest status, not the review status
303 # check only the latest status, not the review status
304 status = ChangesetStatusModel().get_status(
304 status = ChangesetStatusModel().get_status(
305 pull_request.source_repo, pull_request=pull_request)
305 pull_request.source_repo, pull_request=pull_request)
306 assert status == ChangesetStatus.STATUS_REJECTED
306 assert status == ChangesetStatus.STATUS_REJECTED
307
307
308 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
308 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
309 pull_request = pr_util.create_pull_request()
309 pull_request = pr_util.create_pull_request()
310 pull_request_id = pull_request.pull_request_id
310 pull_request_id = pull_request.pull_request_id
311 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
311 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
312 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
312 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
313 author = pull_request.user_id
313 author = pull_request.user_id
314 repo = pull_request.target_repo.repo_id
314 repo = pull_request.target_repo.repo_id
315 self.app.post(
315 self.app.post(
316 url(controller='pullrequests',
316 url(controller='pullrequests',
317 action='comment',
317 action='comment',
318 repo_name=pull_request.target_repo.scm_instance().name,
318 repo_name=pull_request.target_repo.scm_instance().name,
319 pull_request_id=str(pull_request_id)),
319 pull_request_id=str(pull_request_id)),
320 params={
320 params={
321 'changeset_status': 'rejected',
321 'changeset_status': 'rejected',
322 'close_pull_request': '1',
322 'close_pull_request': '1',
323 'csrf_token': csrf_token},
323 'csrf_token': csrf_token},
324 status=302)
324 status=302)
325
325
326 pull_request = PullRequest.get(pull_request_id)
326 pull_request = PullRequest.get(pull_request_id)
327
327
328 action = 'user_closed_pull_request:%d' % pull_request_id
328 action = 'user_closed_pull_request:%d' % pull_request_id
329 journal = UserLog.query().filter(
329 journal = UserLog.query().filter(
330 UserLog.user_id == author,
330 UserLog.user_id == author,
331 UserLog.repository_id == repo,
331 UserLog.repository_id == repo,
332 UserLog.action == action).all()
332 UserLog.action == action).all()
333 assert len(journal) == 1
333 assert len(journal) == 1
334
334
335 # check only the latest status, not the review status
335 # check only the latest status, not the review status
336 status = ChangesetStatusModel().get_status(
336 status = ChangesetStatusModel().get_status(
337 pull_request.source_repo, pull_request=pull_request)
337 pull_request.source_repo, pull_request=pull_request)
338 assert status == ChangesetStatus.STATUS_REJECTED
338 assert status == ChangesetStatus.STATUS_REJECTED
339
339
340 def test_create_pull_request(self, backend, csrf_token):
340 def test_create_pull_request(self, backend, csrf_token):
341 commits = [
341 commits = [
342 {'message': 'ancestor'},
342 {'message': 'ancestor'},
343 {'message': 'change'},
343 {'message': 'change'},
344 {'message': 'change2'},
344 {'message': 'change2'},
345 ]
345 ]
346 commit_ids = backend.create_master_repo(commits)
346 commit_ids = backend.create_master_repo(commits)
347 target = backend.create_repo(heads=['ancestor'])
347 target = backend.create_repo(heads=['ancestor'])
348 source = backend.create_repo(heads=['change2'])
348 source = backend.create_repo(heads=['change2'])
349
349
350 response = self.app.post(
350 response = self.app.post(
351 url(
351 url(
352 controller='pullrequests',
352 controller='pullrequests',
353 action='create',
353 action='create',
354 repo_name=source.repo_name
354 repo_name=source.repo_name
355 ),
355 ),
356 [
356 [
357 ('source_repo', source.repo_name),
357 ('source_repo', source.repo_name),
358 ('source_ref', 'branch:default:' + commit_ids['change2']),
358 ('source_ref', 'branch:default:' + commit_ids['change2']),
359 ('target_repo', target.repo_name),
359 ('target_repo', target.repo_name),
360 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
360 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
361 ('pullrequest_desc', 'Description'),
361 ('pullrequest_desc', 'Description'),
362 ('pullrequest_title', 'Title'),
362 ('pullrequest_title', 'Title'),
363 ('__start__', 'review_members:sequence'),
363 ('__start__', 'review_members:sequence'),
364 ('__start__', 'reviewer:mapping'),
364 ('__start__', 'reviewer:mapping'),
365 ('user_id', '1'),
365 ('user_id', '1'),
366 ('__start__', 'reasons:sequence'),
366 ('__start__', 'reasons:sequence'),
367 ('reason', 'Some reason'),
367 ('reason', 'Some reason'),
368 ('__end__', 'reasons:sequence'),
368 ('__end__', 'reasons:sequence'),
369 ('__end__', 'reviewer:mapping'),
369 ('__end__', 'reviewer:mapping'),
370 ('__end__', 'review_members:sequence'),
370 ('__end__', 'review_members:sequence'),
371 ('__start__', 'revisions:sequence'),
371 ('__start__', 'revisions:sequence'),
372 ('revisions', commit_ids['change']),
372 ('revisions', commit_ids['change']),
373 ('revisions', commit_ids['change2']),
373 ('revisions', commit_ids['change2']),
374 ('__end__', 'revisions:sequence'),
374 ('__end__', 'revisions:sequence'),
375 ('user', ''),
375 ('user', ''),
376 ('csrf_token', csrf_token),
376 ('csrf_token', csrf_token),
377 ],
377 ],
378 status=302)
378 status=302)
379
379
380 location = response.headers['Location']
380 location = response.headers['Location']
381 pull_request_id = int(location.rsplit('/', 1)[1])
381 pull_request_id = int(location.rsplit('/', 1)[1])
382 pull_request = PullRequest.get(pull_request_id)
382 pull_request = PullRequest.get(pull_request_id)
383
383
384 # check that we have now both revisions
384 # check that we have now both revisions
385 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
385 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
386 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
386 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
387 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
387 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
388 assert pull_request.target_ref == expected_target_ref
388 assert pull_request.target_ref == expected_target_ref
389
389
390 def test_reviewer_notifications(self, backend, csrf_token):
390 def test_reviewer_notifications(self, backend, csrf_token):
391 # We have to use the app.post for this test so it will create the
391 # We have to use the app.post for this test so it will create the
392 # notifications properly with the new PR
392 # notifications properly with the new PR
393 commits = [
393 commits = [
394 {'message': 'ancestor',
394 {'message': 'ancestor',
395 'added': [FileNode('file_A', content='content_of_ancestor')]},
395 'added': [FileNode('file_A', content='content_of_ancestor')]},
396 {'message': 'change',
396 {'message': 'change',
397 'added': [FileNode('file_a', content='content_of_change')]},
397 'added': [FileNode('file_a', content='content_of_change')]},
398 {'message': 'change-child'},
398 {'message': 'change-child'},
399 {'message': 'ancestor-child', 'parents': ['ancestor'],
399 {'message': 'ancestor-child', 'parents': ['ancestor'],
400 'added': [
400 'added': [
401 FileNode('file_B', content='content_of_ancestor_child')]},
401 FileNode('file_B', content='content_of_ancestor_child')]},
402 {'message': 'ancestor-child-2'},
402 {'message': 'ancestor-child-2'},
403 ]
403 ]
404 commit_ids = backend.create_master_repo(commits)
404 commit_ids = backend.create_master_repo(commits)
405 target = backend.create_repo(heads=['ancestor-child'])
405 target = backend.create_repo(heads=['ancestor-child'])
406 source = backend.create_repo(heads=['change'])
406 source = backend.create_repo(heads=['change'])
407
407
408 response = self.app.post(
408 response = self.app.post(
409 url(
409 url(
410 controller='pullrequests',
410 controller='pullrequests',
411 action='create',
411 action='create',
412 repo_name=source.repo_name
412 repo_name=source.repo_name
413 ),
413 ),
414 [
414 [
415 ('source_repo', source.repo_name),
415 ('source_repo', source.repo_name),
416 ('source_ref', 'branch:default:' + commit_ids['change']),
416 ('source_ref', 'branch:default:' + commit_ids['change']),
417 ('target_repo', target.repo_name),
417 ('target_repo', target.repo_name),
418 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
418 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
419 ('pullrequest_desc', 'Description'),
419 ('pullrequest_desc', 'Description'),
420 ('pullrequest_title', 'Title'),
420 ('pullrequest_title', 'Title'),
421 ('__start__', 'review_members:sequence'),
421 ('__start__', 'review_members:sequence'),
422 ('__start__', 'reviewer:mapping'),
422 ('__start__', 'reviewer:mapping'),
423 ('user_id', '2'),
423 ('user_id', '2'),
424 ('__start__', 'reasons:sequence'),
424 ('__start__', 'reasons:sequence'),
425 ('reason', 'Some reason'),
425 ('reason', 'Some reason'),
426 ('__end__', 'reasons:sequence'),
426 ('__end__', 'reasons:sequence'),
427 ('__end__', 'reviewer:mapping'),
427 ('__end__', 'reviewer:mapping'),
428 ('__end__', 'review_members:sequence'),
428 ('__end__', 'review_members:sequence'),
429 ('__start__', 'revisions:sequence'),
429 ('__start__', 'revisions:sequence'),
430 ('revisions', commit_ids['change']),
430 ('revisions', commit_ids['change']),
431 ('__end__', 'revisions:sequence'),
431 ('__end__', 'revisions:sequence'),
432 ('user', ''),
432 ('user', ''),
433 ('csrf_token', csrf_token),
433 ('csrf_token', csrf_token),
434 ],
434 ],
435 status=302)
435 status=302)
436
436
437 location = response.headers['Location']
437 location = response.headers['Location']
438 pull_request_id = int(location.rsplit('/', 1)[1])
438 pull_request_id = int(location.rsplit('/', 1)[1])
439 pull_request = PullRequest.get(pull_request_id)
439 pull_request = PullRequest.get(pull_request_id)
440
440
441 # Check that a notification was made
441 # Check that a notification was made
442 notifications = Notification.query()\
442 notifications = Notification.query()\
443 .filter(Notification.created_by == pull_request.author.user_id,
443 .filter(Notification.created_by == pull_request.author.user_id,
444 Notification.type_ == Notification.TYPE_PULL_REQUEST,
444 Notification.type_ == Notification.TYPE_PULL_REQUEST,
445 Notification.subject.contains("wants you to review "
445 Notification.subject.contains("wants you to review "
446 "pull request #%d"
446 "pull request #%d"
447 % pull_request_id))
447 % pull_request_id))
448 assert len(notifications.all()) == 1
448 assert len(notifications.all()) == 1
449
449
450 # Change reviewers and check that a notification was made
450 # Change reviewers and check that a notification was made
451 PullRequestModel().update_reviewers(
451 PullRequestModel().update_reviewers(
452 pull_request.pull_request_id, [(1, [])])
452 pull_request.pull_request_id, [(1, [])])
453 assert len(notifications.all()) == 2
453 assert len(notifications.all()) == 2
454
454
455 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
455 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
456 csrf_token):
456 csrf_token):
457 commits = [
457 commits = [
458 {'message': 'ancestor',
458 {'message': 'ancestor',
459 'added': [FileNode('file_A', content='content_of_ancestor')]},
459 'added': [FileNode('file_A', content='content_of_ancestor')]},
460 {'message': 'change',
460 {'message': 'change',
461 'added': [FileNode('file_a', content='content_of_change')]},
461 'added': [FileNode('file_a', content='content_of_change')]},
462 {'message': 'change-child'},
462 {'message': 'change-child'},
463 {'message': 'ancestor-child', 'parents': ['ancestor'],
463 {'message': 'ancestor-child', 'parents': ['ancestor'],
464 'added': [
464 'added': [
465 FileNode('file_B', content='content_of_ancestor_child')]},
465 FileNode('file_B', content='content_of_ancestor_child')]},
466 {'message': 'ancestor-child-2'},
466 {'message': 'ancestor-child-2'},
467 ]
467 ]
468 commit_ids = backend.create_master_repo(commits)
468 commit_ids = backend.create_master_repo(commits)
469 target = backend.create_repo(heads=['ancestor-child'])
469 target = backend.create_repo(heads=['ancestor-child'])
470 source = backend.create_repo(heads=['change'])
470 source = backend.create_repo(heads=['change'])
471
471
472 response = self.app.post(
472 response = self.app.post(
473 url(
473 url(
474 controller='pullrequests',
474 controller='pullrequests',
475 action='create',
475 action='create',
476 repo_name=source.repo_name
476 repo_name=source.repo_name
477 ),
477 ),
478 [
478 [
479 ('source_repo', source.repo_name),
479 ('source_repo', source.repo_name),
480 ('source_ref', 'branch:default:' + commit_ids['change']),
480 ('source_ref', 'branch:default:' + commit_ids['change']),
481 ('target_repo', target.repo_name),
481 ('target_repo', target.repo_name),
482 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
482 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
483 ('pullrequest_desc', 'Description'),
483 ('pullrequest_desc', 'Description'),
484 ('pullrequest_title', 'Title'),
484 ('pullrequest_title', 'Title'),
485 ('__start__', 'review_members:sequence'),
485 ('__start__', 'review_members:sequence'),
486 ('__start__', 'reviewer:mapping'),
486 ('__start__', 'reviewer:mapping'),
487 ('user_id', '1'),
487 ('user_id', '1'),
488 ('__start__', 'reasons:sequence'),
488 ('__start__', 'reasons:sequence'),
489 ('reason', 'Some reason'),
489 ('reason', 'Some reason'),
490 ('__end__', 'reasons:sequence'),
490 ('__end__', 'reasons:sequence'),
491 ('__end__', 'reviewer:mapping'),
491 ('__end__', 'reviewer:mapping'),
492 ('__end__', 'review_members:sequence'),
492 ('__end__', 'review_members:sequence'),
493 ('__start__', 'revisions:sequence'),
493 ('__start__', 'revisions:sequence'),
494 ('revisions', commit_ids['change']),
494 ('revisions', commit_ids['change']),
495 ('__end__', 'revisions:sequence'),
495 ('__end__', 'revisions:sequence'),
496 ('user', ''),
496 ('user', ''),
497 ('csrf_token', csrf_token),
497 ('csrf_token', csrf_token),
498 ],
498 ],
499 status=302)
499 status=302)
500
500
501 location = response.headers['Location']
501 location = response.headers['Location']
502 pull_request_id = int(location.rsplit('/', 1)[1])
502 pull_request_id = int(location.rsplit('/', 1)[1])
503 pull_request = PullRequest.get(pull_request_id)
503 pull_request = PullRequest.get(pull_request_id)
504
504
505 # target_ref has to point to the ancestor's commit_id in order to
505 # target_ref has to point to the ancestor's commit_id in order to
506 # show the correct diff
506 # show the correct diff
507 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
507 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
508 assert pull_request.target_ref == expected_target_ref
508 assert pull_request.target_ref == expected_target_ref
509
509
510 # Check generated diff contents
510 # Check generated diff contents
511 response = response.follow()
511 response = response.follow()
512 assert 'content_of_ancestor' not in response.body
512 assert 'content_of_ancestor' not in response.body
513 assert 'content_of_ancestor-child' not in response.body
513 assert 'content_of_ancestor-child' not in response.body
514 assert 'content_of_change' in response.body
514 assert 'content_of_change' in response.body
515
515
516 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
516 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
517 # Clear any previous calls to rcextensions
517 # Clear any previous calls to rcextensions
518 rhodecode.EXTENSIONS.calls.clear()
518 rhodecode.EXTENSIONS.calls.clear()
519
519
520 pull_request = pr_util.create_pull_request(
520 pull_request = pr_util.create_pull_request(
521 approved=True, mergeable=True)
521 approved=True, mergeable=True)
522 pull_request_id = pull_request.pull_request_id
522 pull_request_id = pull_request.pull_request_id
523 repo_name = pull_request.target_repo.scm_instance().name,
523 repo_name = pull_request.target_repo.scm_instance().name,
524
524
525 response = self.app.post(
525 response = self.app.post(
526 url(controller='pullrequests',
526 url(controller='pullrequests',
527 action='merge',
527 action='merge',
528 repo_name=str(repo_name[0]),
528 repo_name=str(repo_name[0]),
529 pull_request_id=str(pull_request_id)),
529 pull_request_id=str(pull_request_id)),
530 params={'csrf_token': csrf_token}).follow()
530 params={'csrf_token': csrf_token}).follow()
531
531
532 pull_request = PullRequest.get(pull_request_id)
532 pull_request = PullRequest.get(pull_request_id)
533
533
534 assert response.status_int == 200
534 assert response.status_int == 200
535 assert pull_request.is_closed()
535 assert pull_request.is_closed()
536 assert_pull_request_status(
536 assert_pull_request_status(
537 pull_request, ChangesetStatus.STATUS_APPROVED)
537 pull_request, ChangesetStatus.STATUS_APPROVED)
538
538
539 # Check the relevant log entries were added
539 # Check the relevant log entries were added
540 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
540 user_logs = UserLog.query() \
541 .filter(UserLog.version == UserLog.VERSION_1) \
542 .order_by('-user_log_id').limit(4)
541 actions = [log.action for log in user_logs]
543 actions = [log.action for log in user_logs]
542 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
544 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
543 expected_actions = [
545 expected_actions = [
544 u'user_closed_pull_request:%d' % pull_request_id,
546 u'user_closed_pull_request:%d' % pull_request_id,
545 u'user_merged_pull_request:%d' % pull_request_id,
547 u'user_merged_pull_request:%d' % pull_request_id,
546 # The action below reflect that the post push actions were executed
548 # The action below reflect that the post push actions were executed
547 u'user_commented_pull_request:%d' % pull_request_id,
549 u'user_commented_pull_request:%d' % pull_request_id,
548 u'push:%s' % ','.join(pr_commit_ids),
550 u'push:%s' % ','.join(pr_commit_ids),
549 ]
551 ]
550 assert actions == expected_actions
552 assert actions == expected_actions
551
553
552 # Check post_push rcextension was really executed
554 # Check post_push rcextension was really executed
553 push_calls = rhodecode.EXTENSIONS.calls['post_push']
555 push_calls = rhodecode.EXTENSIONS.calls['post_push']
554 assert len(push_calls) == 1
556 assert len(push_calls) == 1
555 unused_last_call_args, last_call_kwargs = push_calls[0]
557 unused_last_call_args, last_call_kwargs = push_calls[0]
556 assert last_call_kwargs['action'] == 'push'
558 assert last_call_kwargs['action'] == 'push'
557 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
559 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
558
560
559 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
561 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
560 pull_request = pr_util.create_pull_request(mergeable=False)
562 pull_request = pr_util.create_pull_request(mergeable=False)
561 pull_request_id = pull_request.pull_request_id
563 pull_request_id = pull_request.pull_request_id
562 pull_request = PullRequest.get(pull_request_id)
564 pull_request = PullRequest.get(pull_request_id)
563
565
564 response = self.app.post(
566 response = self.app.post(
565 url(controller='pullrequests',
567 url(controller='pullrequests',
566 action='merge',
568 action='merge',
567 repo_name=pull_request.target_repo.scm_instance().name,
569 repo_name=pull_request.target_repo.scm_instance().name,
568 pull_request_id=str(pull_request.pull_request_id)),
570 pull_request_id=str(pull_request.pull_request_id)),
569 params={'csrf_token': csrf_token}).follow()
571 params={'csrf_token': csrf_token}).follow()
570
572
571 assert response.status_int == 200
573 assert response.status_int == 200
572 response.mustcontain(
574 response.mustcontain(
573 'Merge is not currently possible because of below failed checks.')
575 'Merge is not currently possible because of below failed checks.')
574 response.mustcontain('Server-side pull request merging is disabled.')
576 response.mustcontain('Server-side pull request merging is disabled.')
575
577
576 @pytest.mark.skip_backends('svn')
578 @pytest.mark.skip_backends('svn')
577 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
579 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
578 pull_request = pr_util.create_pull_request(mergeable=True)
580 pull_request = pr_util.create_pull_request(mergeable=True)
579 pull_request_id = pull_request.pull_request_id
581 pull_request_id = pull_request.pull_request_id
580 repo_name = pull_request.target_repo.scm_instance().name,
582 repo_name = pull_request.target_repo.scm_instance().name,
581
583
582 response = self.app.post(
584 response = self.app.post(
583 url(controller='pullrequests',
585 url(controller='pullrequests',
584 action='merge',
586 action='merge',
585 repo_name=str(repo_name[0]),
587 repo_name=str(repo_name[0]),
586 pull_request_id=str(pull_request_id)),
588 pull_request_id=str(pull_request_id)),
587 params={'csrf_token': csrf_token}).follow()
589 params={'csrf_token': csrf_token}).follow()
588
590
589 assert response.status_int == 200
591 assert response.status_int == 200
590
592
591 response.mustcontain(
593 response.mustcontain(
592 'Merge is not currently possible because of below failed checks.')
594 'Merge is not currently possible because of below failed checks.')
593 response.mustcontain('Pull request reviewer approval is pending.')
595 response.mustcontain('Pull request reviewer approval is pending.')
594
596
595 def test_update_source_revision(self, backend, csrf_token):
597 def test_update_source_revision(self, backend, csrf_token):
596 commits = [
598 commits = [
597 {'message': 'ancestor'},
599 {'message': 'ancestor'},
598 {'message': 'change'},
600 {'message': 'change'},
599 {'message': 'change-2'},
601 {'message': 'change-2'},
600 ]
602 ]
601 commit_ids = backend.create_master_repo(commits)
603 commit_ids = backend.create_master_repo(commits)
602 target = backend.create_repo(heads=['ancestor'])
604 target = backend.create_repo(heads=['ancestor'])
603 source = backend.create_repo(heads=['change'])
605 source = backend.create_repo(heads=['change'])
604
606
605 # create pr from a in source to A in target
607 # create pr from a in source to A in target
606 pull_request = PullRequest()
608 pull_request = PullRequest()
607 pull_request.source_repo = source
609 pull_request.source_repo = source
608 # TODO: johbo: Make sure that we write the source ref this way!
610 # TODO: johbo: Make sure that we write the source ref this way!
609 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
611 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
610 branch=backend.default_branch_name, commit_id=commit_ids['change'])
612 branch=backend.default_branch_name, commit_id=commit_ids['change'])
611 pull_request.target_repo = target
613 pull_request.target_repo = target
612
614
613 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
615 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
614 branch=backend.default_branch_name,
616 branch=backend.default_branch_name,
615 commit_id=commit_ids['ancestor'])
617 commit_id=commit_ids['ancestor'])
616 pull_request.revisions = [commit_ids['change']]
618 pull_request.revisions = [commit_ids['change']]
617 pull_request.title = u"Test"
619 pull_request.title = u"Test"
618 pull_request.description = u"Description"
620 pull_request.description = u"Description"
619 pull_request.author = UserModel().get_by_username(
621 pull_request.author = UserModel().get_by_username(
620 TEST_USER_ADMIN_LOGIN)
622 TEST_USER_ADMIN_LOGIN)
621 Session().add(pull_request)
623 Session().add(pull_request)
622 Session().commit()
624 Session().commit()
623 pull_request_id = pull_request.pull_request_id
625 pull_request_id = pull_request.pull_request_id
624
626
625 # source has ancestor - change - change-2
627 # source has ancestor - change - change-2
626 backend.pull_heads(source, heads=['change-2'])
628 backend.pull_heads(source, heads=['change-2'])
627
629
628 # update PR
630 # update PR
629 self.app.post(
631 self.app.post(
630 url(controller='pullrequests', action='update',
632 url(controller='pullrequests', action='update',
631 repo_name=target.repo_name,
633 repo_name=target.repo_name,
632 pull_request_id=str(pull_request_id)),
634 pull_request_id=str(pull_request_id)),
633 params={'update_commits': 'true', '_method': 'put',
635 params={'update_commits': 'true', '_method': 'put',
634 'csrf_token': csrf_token})
636 'csrf_token': csrf_token})
635
637
636 # check that we have now both revisions
638 # check that we have now both revisions
637 pull_request = PullRequest.get(pull_request_id)
639 pull_request = PullRequest.get(pull_request_id)
638 assert pull_request.revisions == [
640 assert pull_request.revisions == [
639 commit_ids['change-2'], commit_ids['change']]
641 commit_ids['change-2'], commit_ids['change']]
640
642
641 # TODO: johbo: this should be a test on its own
643 # TODO: johbo: this should be a test on its own
642 response = self.app.get(url(
644 response = self.app.get(url(
643 controller='pullrequests', action='index',
645 controller='pullrequests', action='index',
644 repo_name=target.repo_name))
646 repo_name=target.repo_name))
645 assert response.status_int == 200
647 assert response.status_int == 200
646 assert 'Pull request updated to' in response.body
648 assert 'Pull request updated to' in response.body
647 assert 'with 1 added, 0 removed commits.' in response.body
649 assert 'with 1 added, 0 removed commits.' in response.body
648
650
649 def test_update_target_revision(self, backend, csrf_token):
651 def test_update_target_revision(self, backend, csrf_token):
650 commits = [
652 commits = [
651 {'message': 'ancestor'},
653 {'message': 'ancestor'},
652 {'message': 'change'},
654 {'message': 'change'},
653 {'message': 'ancestor-new', 'parents': ['ancestor']},
655 {'message': 'ancestor-new', 'parents': ['ancestor']},
654 {'message': 'change-rebased'},
656 {'message': 'change-rebased'},
655 ]
657 ]
656 commit_ids = backend.create_master_repo(commits)
658 commit_ids = backend.create_master_repo(commits)
657 target = backend.create_repo(heads=['ancestor'])
659 target = backend.create_repo(heads=['ancestor'])
658 source = backend.create_repo(heads=['change'])
660 source = backend.create_repo(heads=['change'])
659
661
660 # create pr from a in source to A in target
662 # create pr from a in source to A in target
661 pull_request = PullRequest()
663 pull_request = PullRequest()
662 pull_request.source_repo = source
664 pull_request.source_repo = source
663 # TODO: johbo: Make sure that we write the source ref this way!
665 # TODO: johbo: Make sure that we write the source ref this way!
664 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
666 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
665 branch=backend.default_branch_name, commit_id=commit_ids['change'])
667 branch=backend.default_branch_name, commit_id=commit_ids['change'])
666 pull_request.target_repo = target
668 pull_request.target_repo = target
667 # TODO: johbo: Target ref should be branch based, since tip can jump
669 # TODO: johbo: Target ref should be branch based, since tip can jump
668 # from branch to branch
670 # from branch to branch
669 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
671 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
670 branch=backend.default_branch_name,
672 branch=backend.default_branch_name,
671 commit_id=commit_ids['ancestor'])
673 commit_id=commit_ids['ancestor'])
672 pull_request.revisions = [commit_ids['change']]
674 pull_request.revisions = [commit_ids['change']]
673 pull_request.title = u"Test"
675 pull_request.title = u"Test"
674 pull_request.description = u"Description"
676 pull_request.description = u"Description"
675 pull_request.author = UserModel().get_by_username(
677 pull_request.author = UserModel().get_by_username(
676 TEST_USER_ADMIN_LOGIN)
678 TEST_USER_ADMIN_LOGIN)
677 Session().add(pull_request)
679 Session().add(pull_request)
678 Session().commit()
680 Session().commit()
679 pull_request_id = pull_request.pull_request_id
681 pull_request_id = pull_request.pull_request_id
680
682
681 # target has ancestor - ancestor-new
683 # target has ancestor - ancestor-new
682 # source has ancestor - ancestor-new - change-rebased
684 # source has ancestor - ancestor-new - change-rebased
683 backend.pull_heads(target, heads=['ancestor-new'])
685 backend.pull_heads(target, heads=['ancestor-new'])
684 backend.pull_heads(source, heads=['change-rebased'])
686 backend.pull_heads(source, heads=['change-rebased'])
685
687
686 # update PR
688 # update PR
687 self.app.post(
689 self.app.post(
688 url(controller='pullrequests', action='update',
690 url(controller='pullrequests', action='update',
689 repo_name=target.repo_name,
691 repo_name=target.repo_name,
690 pull_request_id=str(pull_request_id)),
692 pull_request_id=str(pull_request_id)),
691 params={'update_commits': 'true', '_method': 'put',
693 params={'update_commits': 'true', '_method': 'put',
692 'csrf_token': csrf_token},
694 'csrf_token': csrf_token},
693 status=200)
695 status=200)
694
696
695 # check that we have now both revisions
697 # check that we have now both revisions
696 pull_request = PullRequest.get(pull_request_id)
698 pull_request = PullRequest.get(pull_request_id)
697 assert pull_request.revisions == [commit_ids['change-rebased']]
699 assert pull_request.revisions == [commit_ids['change-rebased']]
698 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
700 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
699 branch=backend.default_branch_name,
701 branch=backend.default_branch_name,
700 commit_id=commit_ids['ancestor-new'])
702 commit_id=commit_ids['ancestor-new'])
701
703
702 # TODO: johbo: This should be a test on its own
704 # TODO: johbo: This should be a test on its own
703 response = self.app.get(url(
705 response = self.app.get(url(
704 controller='pullrequests', action='index',
706 controller='pullrequests', action='index',
705 repo_name=target.repo_name))
707 repo_name=target.repo_name))
706 assert response.status_int == 200
708 assert response.status_int == 200
707 assert 'Pull request updated to' in response.body
709 assert 'Pull request updated to' in response.body
708 assert 'with 1 added, 1 removed commits.' in response.body
710 assert 'with 1 added, 1 removed commits.' in response.body
709
711
710 def test_update_of_ancestor_reference(self, backend, csrf_token):
712 def test_update_of_ancestor_reference(self, backend, csrf_token):
711 commits = [
713 commits = [
712 {'message': 'ancestor'},
714 {'message': 'ancestor'},
713 {'message': 'change'},
715 {'message': 'change'},
714 {'message': 'change-2'},
716 {'message': 'change-2'},
715 {'message': 'ancestor-new', 'parents': ['ancestor']},
717 {'message': 'ancestor-new', 'parents': ['ancestor']},
716 {'message': 'change-rebased'},
718 {'message': 'change-rebased'},
717 ]
719 ]
718 commit_ids = backend.create_master_repo(commits)
720 commit_ids = backend.create_master_repo(commits)
719 target = backend.create_repo(heads=['ancestor'])
721 target = backend.create_repo(heads=['ancestor'])
720 source = backend.create_repo(heads=['change'])
722 source = backend.create_repo(heads=['change'])
721
723
722 # create pr from a in source to A in target
724 # create pr from a in source to A in target
723 pull_request = PullRequest()
725 pull_request = PullRequest()
724 pull_request.source_repo = source
726 pull_request.source_repo = source
725 # TODO: johbo: Make sure that we write the source ref this way!
727 # TODO: johbo: Make sure that we write the source ref this way!
726 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
728 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
727 branch=backend.default_branch_name,
729 branch=backend.default_branch_name,
728 commit_id=commit_ids['change'])
730 commit_id=commit_ids['change'])
729 pull_request.target_repo = target
731 pull_request.target_repo = target
730 # TODO: johbo: Target ref should be branch based, since tip can jump
732 # TODO: johbo: Target ref should be branch based, since tip can jump
731 # from branch to branch
733 # from branch to branch
732 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
734 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
733 branch=backend.default_branch_name,
735 branch=backend.default_branch_name,
734 commit_id=commit_ids['ancestor'])
736 commit_id=commit_ids['ancestor'])
735 pull_request.revisions = [commit_ids['change']]
737 pull_request.revisions = [commit_ids['change']]
736 pull_request.title = u"Test"
738 pull_request.title = u"Test"
737 pull_request.description = u"Description"
739 pull_request.description = u"Description"
738 pull_request.author = UserModel().get_by_username(
740 pull_request.author = UserModel().get_by_username(
739 TEST_USER_ADMIN_LOGIN)
741 TEST_USER_ADMIN_LOGIN)
740 Session().add(pull_request)
742 Session().add(pull_request)
741 Session().commit()
743 Session().commit()
742 pull_request_id = pull_request.pull_request_id
744 pull_request_id = pull_request.pull_request_id
743
745
744 # target has ancestor - ancestor-new
746 # target has ancestor - ancestor-new
745 # source has ancestor - ancestor-new - change-rebased
747 # source has ancestor - ancestor-new - change-rebased
746 backend.pull_heads(target, heads=['ancestor-new'])
748 backend.pull_heads(target, heads=['ancestor-new'])
747 backend.pull_heads(source, heads=['change-rebased'])
749 backend.pull_heads(source, heads=['change-rebased'])
748
750
749 # update PR
751 # update PR
750 self.app.post(
752 self.app.post(
751 url(controller='pullrequests', action='update',
753 url(controller='pullrequests', action='update',
752 repo_name=target.repo_name,
754 repo_name=target.repo_name,
753 pull_request_id=str(pull_request_id)),
755 pull_request_id=str(pull_request_id)),
754 params={'update_commits': 'true', '_method': 'put',
756 params={'update_commits': 'true', '_method': 'put',
755 'csrf_token': csrf_token},
757 'csrf_token': csrf_token},
756 status=200)
758 status=200)
757
759
758 # Expect the target reference to be updated correctly
760 # Expect the target reference to be updated correctly
759 pull_request = PullRequest.get(pull_request_id)
761 pull_request = PullRequest.get(pull_request_id)
760 assert pull_request.revisions == [commit_ids['change-rebased']]
762 assert pull_request.revisions == [commit_ids['change-rebased']]
761 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
763 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
762 branch=backend.default_branch_name,
764 branch=backend.default_branch_name,
763 commit_id=commit_ids['ancestor-new'])
765 commit_id=commit_ids['ancestor-new'])
764 assert pull_request.target_ref == expected_target_ref
766 assert pull_request.target_ref == expected_target_ref
765
767
766 def test_remove_pull_request_branch(self, backend_git, csrf_token):
768 def test_remove_pull_request_branch(self, backend_git, csrf_token):
767 branch_name = 'development'
769 branch_name = 'development'
768 commits = [
770 commits = [
769 {'message': 'initial-commit'},
771 {'message': 'initial-commit'},
770 {'message': 'old-feature'},
772 {'message': 'old-feature'},
771 {'message': 'new-feature', 'branch': branch_name},
773 {'message': 'new-feature', 'branch': branch_name},
772 ]
774 ]
773 repo = backend_git.create_repo(commits)
775 repo = backend_git.create_repo(commits)
774 commit_ids = backend_git.commit_ids
776 commit_ids = backend_git.commit_ids
775
777
776 pull_request = PullRequest()
778 pull_request = PullRequest()
777 pull_request.source_repo = repo
779 pull_request.source_repo = repo
778 pull_request.target_repo = repo
780 pull_request.target_repo = repo
779 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
781 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
780 branch=branch_name, commit_id=commit_ids['new-feature'])
782 branch=branch_name, commit_id=commit_ids['new-feature'])
781 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
783 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
782 branch=backend_git.default_branch_name,
784 branch=backend_git.default_branch_name,
783 commit_id=commit_ids['old-feature'])
785 commit_id=commit_ids['old-feature'])
784 pull_request.revisions = [commit_ids['new-feature']]
786 pull_request.revisions = [commit_ids['new-feature']]
785 pull_request.title = u"Test"
787 pull_request.title = u"Test"
786 pull_request.description = u"Description"
788 pull_request.description = u"Description"
787 pull_request.author = UserModel().get_by_username(
789 pull_request.author = UserModel().get_by_username(
788 TEST_USER_ADMIN_LOGIN)
790 TEST_USER_ADMIN_LOGIN)
789 Session().add(pull_request)
791 Session().add(pull_request)
790 Session().commit()
792 Session().commit()
791
793
792 vcs = repo.scm_instance()
794 vcs = repo.scm_instance()
793 vcs.remove_ref('refs/heads/{}'.format(branch_name))
795 vcs.remove_ref('refs/heads/{}'.format(branch_name))
794
796
795 response = self.app.get(url(
797 response = self.app.get(url(
796 controller='pullrequests', action='show',
798 controller='pullrequests', action='show',
797 repo_name=repo.repo_name,
799 repo_name=repo.repo_name,
798 pull_request_id=str(pull_request.pull_request_id)))
800 pull_request_id=str(pull_request.pull_request_id)))
799
801
800 assert response.status_int == 200
802 assert response.status_int == 200
801 assert_response = AssertResponse(response)
803 assert_response = AssertResponse(response)
802 assert_response.element_contains(
804 assert_response.element_contains(
803 '#changeset_compare_view_content .alert strong',
805 '#changeset_compare_view_content .alert strong',
804 'Missing commits')
806 'Missing commits')
805 assert_response.element_contains(
807 assert_response.element_contains(
806 '#changeset_compare_view_content .alert',
808 '#changeset_compare_view_content .alert',
807 'This pull request cannot be displayed, because one or more'
809 'This pull request cannot be displayed, because one or more'
808 ' commits no longer exist in the source repository.')
810 ' commits no longer exist in the source repository.')
809
811
810 def test_strip_commits_from_pull_request(
812 def test_strip_commits_from_pull_request(
811 self, backend, pr_util, csrf_token):
813 self, backend, pr_util, csrf_token):
812 commits = [
814 commits = [
813 {'message': 'initial-commit'},
815 {'message': 'initial-commit'},
814 {'message': 'old-feature'},
816 {'message': 'old-feature'},
815 {'message': 'new-feature', 'parents': ['initial-commit']},
817 {'message': 'new-feature', 'parents': ['initial-commit']},
816 ]
818 ]
817 pull_request = pr_util.create_pull_request(
819 pull_request = pr_util.create_pull_request(
818 commits, target_head='initial-commit', source_head='new-feature',
820 commits, target_head='initial-commit', source_head='new-feature',
819 revisions=['new-feature'])
821 revisions=['new-feature'])
820
822
821 vcs = pr_util.source_repository.scm_instance()
823 vcs = pr_util.source_repository.scm_instance()
822 if backend.alias == 'git':
824 if backend.alias == 'git':
823 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
825 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
824 else:
826 else:
825 vcs.strip(pr_util.commit_ids['new-feature'])
827 vcs.strip(pr_util.commit_ids['new-feature'])
826
828
827 response = self.app.get(url(
829 response = self.app.get(url(
828 controller='pullrequests', action='show',
830 controller='pullrequests', action='show',
829 repo_name=pr_util.target_repository.repo_name,
831 repo_name=pr_util.target_repository.repo_name,
830 pull_request_id=str(pull_request.pull_request_id)))
832 pull_request_id=str(pull_request.pull_request_id)))
831
833
832 assert response.status_int == 200
834 assert response.status_int == 200
833 assert_response = AssertResponse(response)
835 assert_response = AssertResponse(response)
834 assert_response.element_contains(
836 assert_response.element_contains(
835 '#changeset_compare_view_content .alert strong',
837 '#changeset_compare_view_content .alert strong',
836 'Missing commits')
838 'Missing commits')
837 assert_response.element_contains(
839 assert_response.element_contains(
838 '#changeset_compare_view_content .alert',
840 '#changeset_compare_view_content .alert',
839 'This pull request cannot be displayed, because one or more'
841 'This pull request cannot be displayed, because one or more'
840 ' commits no longer exist in the source repository.')
842 ' commits no longer exist in the source repository.')
841 assert_response.element_contains(
843 assert_response.element_contains(
842 '#update_commits',
844 '#update_commits',
843 'Update commits')
845 'Update commits')
844
846
845 def test_strip_commits_and_update(
847 def test_strip_commits_and_update(
846 self, backend, pr_util, csrf_token):
848 self, backend, pr_util, csrf_token):
847 commits = [
849 commits = [
848 {'message': 'initial-commit'},
850 {'message': 'initial-commit'},
849 {'message': 'old-feature'},
851 {'message': 'old-feature'},
850 {'message': 'new-feature', 'parents': ['old-feature']},
852 {'message': 'new-feature', 'parents': ['old-feature']},
851 ]
853 ]
852 pull_request = pr_util.create_pull_request(
854 pull_request = pr_util.create_pull_request(
853 commits, target_head='old-feature', source_head='new-feature',
855 commits, target_head='old-feature', source_head='new-feature',
854 revisions=['new-feature'], mergeable=True)
856 revisions=['new-feature'], mergeable=True)
855
857
856 vcs = pr_util.source_repository.scm_instance()
858 vcs = pr_util.source_repository.scm_instance()
857 if backend.alias == 'git':
859 if backend.alias == 'git':
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
860 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
859 else:
861 else:
860 vcs.strip(pr_util.commit_ids['new-feature'])
862 vcs.strip(pr_util.commit_ids['new-feature'])
861
863
862 response = self.app.post(
864 response = self.app.post(
863 url(controller='pullrequests', action='update',
865 url(controller='pullrequests', action='update',
864 repo_name=pull_request.target_repo.repo_name,
866 repo_name=pull_request.target_repo.repo_name,
865 pull_request_id=str(pull_request.pull_request_id)),
867 pull_request_id=str(pull_request.pull_request_id)),
866 params={'update_commits': 'true', '_method': 'put',
868 params={'update_commits': 'true', '_method': 'put',
867 'csrf_token': csrf_token})
869 'csrf_token': csrf_token})
868
870
869 assert response.status_int == 200
871 assert response.status_int == 200
870 assert response.body == 'true'
872 assert response.body == 'true'
871
873
872 # Make sure that after update, it won't raise 500 errors
874 # Make sure that after update, it won't raise 500 errors
873 response = self.app.get(url(
875 response = self.app.get(url(
874 controller='pullrequests', action='show',
876 controller='pullrequests', action='show',
875 repo_name=pr_util.target_repository.repo_name,
877 repo_name=pr_util.target_repository.repo_name,
876 pull_request_id=str(pull_request.pull_request_id)))
878 pull_request_id=str(pull_request.pull_request_id)))
877
879
878 assert response.status_int == 200
880 assert response.status_int == 200
879 assert_response = AssertResponse(response)
881 assert_response = AssertResponse(response)
880 assert_response.element_contains(
882 assert_response.element_contains(
881 '#changeset_compare_view_content .alert strong',
883 '#changeset_compare_view_content .alert strong',
882 'Missing commits')
884 'Missing commits')
883
885
884 def test_branch_is_a_link(self, pr_util):
886 def test_branch_is_a_link(self, pr_util):
885 pull_request = pr_util.create_pull_request()
887 pull_request = pr_util.create_pull_request()
886 pull_request.source_ref = 'branch:origin:1234567890abcdef'
888 pull_request.source_ref = 'branch:origin:1234567890abcdef'
887 pull_request.target_ref = 'branch:target:abcdef1234567890'
889 pull_request.target_ref = 'branch:target:abcdef1234567890'
888 Session().add(pull_request)
890 Session().add(pull_request)
889 Session().commit()
891 Session().commit()
890
892
891 response = self.app.get(url(
893 response = self.app.get(url(
892 controller='pullrequests', action='show',
894 controller='pullrequests', action='show',
893 repo_name=pull_request.target_repo.scm_instance().name,
895 repo_name=pull_request.target_repo.scm_instance().name,
894 pull_request_id=str(pull_request.pull_request_id)))
896 pull_request_id=str(pull_request.pull_request_id)))
895 assert response.status_int == 200
897 assert response.status_int == 200
896 assert_response = AssertResponse(response)
898 assert_response = AssertResponse(response)
897
899
898 origin = assert_response.get_element('.pr-origininfo .tag')
900 origin = assert_response.get_element('.pr-origininfo .tag')
899 origin_children = origin.getchildren()
901 origin_children = origin.getchildren()
900 assert len(origin_children) == 1
902 assert len(origin_children) == 1
901 target = assert_response.get_element('.pr-targetinfo .tag')
903 target = assert_response.get_element('.pr-targetinfo .tag')
902 target_children = target.getchildren()
904 target_children = target.getchildren()
903 assert len(target_children) == 1
905 assert len(target_children) == 1
904
906
905 expected_origin_link = url(
907 expected_origin_link = url(
906 'changelog_home',
908 'changelog_home',
907 repo_name=pull_request.source_repo.scm_instance().name,
909 repo_name=pull_request.source_repo.scm_instance().name,
908 branch='origin')
910 branch='origin')
909 expected_target_link = url(
911 expected_target_link = url(
910 'changelog_home',
912 'changelog_home',
911 repo_name=pull_request.target_repo.scm_instance().name,
913 repo_name=pull_request.target_repo.scm_instance().name,
912 branch='target')
914 branch='target')
913 assert origin_children[0].attrib['href'] == expected_origin_link
915 assert origin_children[0].attrib['href'] == expected_origin_link
914 assert origin_children[0].text == 'branch: origin'
916 assert origin_children[0].text == 'branch: origin'
915 assert target_children[0].attrib['href'] == expected_target_link
917 assert target_children[0].attrib['href'] == expected_target_link
916 assert target_children[0].text == 'branch: target'
918 assert target_children[0].text == 'branch: target'
917
919
918 def test_bookmark_is_not_a_link(self, pr_util):
920 def test_bookmark_is_not_a_link(self, pr_util):
919 pull_request = pr_util.create_pull_request()
921 pull_request = pr_util.create_pull_request()
920 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
922 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
921 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
923 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
922 Session().add(pull_request)
924 Session().add(pull_request)
923 Session().commit()
925 Session().commit()
924
926
925 response = self.app.get(url(
927 response = self.app.get(url(
926 controller='pullrequests', action='show',
928 controller='pullrequests', action='show',
927 repo_name=pull_request.target_repo.scm_instance().name,
929 repo_name=pull_request.target_repo.scm_instance().name,
928 pull_request_id=str(pull_request.pull_request_id)))
930 pull_request_id=str(pull_request.pull_request_id)))
929 assert response.status_int == 200
931 assert response.status_int == 200
930 assert_response = AssertResponse(response)
932 assert_response = AssertResponse(response)
931
933
932 origin = assert_response.get_element('.pr-origininfo .tag')
934 origin = assert_response.get_element('.pr-origininfo .tag')
933 assert origin.text.strip() == 'bookmark: origin'
935 assert origin.text.strip() == 'bookmark: origin'
934 assert origin.getchildren() == []
936 assert origin.getchildren() == []
935
937
936 target = assert_response.get_element('.pr-targetinfo .tag')
938 target = assert_response.get_element('.pr-targetinfo .tag')
937 assert target.text.strip() == 'bookmark: target'
939 assert target.text.strip() == 'bookmark: target'
938 assert target.getchildren() == []
940 assert target.getchildren() == []
939
941
940 def test_tag_is_not_a_link(self, pr_util):
942 def test_tag_is_not_a_link(self, pr_util):
941 pull_request = pr_util.create_pull_request()
943 pull_request = pr_util.create_pull_request()
942 pull_request.source_ref = 'tag:origin:1234567890abcdef'
944 pull_request.source_ref = 'tag:origin:1234567890abcdef'
943 pull_request.target_ref = 'tag:target:abcdef1234567890'
945 pull_request.target_ref = 'tag:target:abcdef1234567890'
944 Session().add(pull_request)
946 Session().add(pull_request)
945 Session().commit()
947 Session().commit()
946
948
947 response = self.app.get(url(
949 response = self.app.get(url(
948 controller='pullrequests', action='show',
950 controller='pullrequests', action='show',
949 repo_name=pull_request.target_repo.scm_instance().name,
951 repo_name=pull_request.target_repo.scm_instance().name,
950 pull_request_id=str(pull_request.pull_request_id)))
952 pull_request_id=str(pull_request.pull_request_id)))
951 assert response.status_int == 200
953 assert response.status_int == 200
952 assert_response = AssertResponse(response)
954 assert_response = AssertResponse(response)
953
955
954 origin = assert_response.get_element('.pr-origininfo .tag')
956 origin = assert_response.get_element('.pr-origininfo .tag')
955 assert origin.text.strip() == 'tag: origin'
957 assert origin.text.strip() == 'tag: origin'
956 assert origin.getchildren() == []
958 assert origin.getchildren() == []
957
959
958 target = assert_response.get_element('.pr-targetinfo .tag')
960 target = assert_response.get_element('.pr-targetinfo .tag')
959 assert target.text.strip() == 'tag: target'
961 assert target.text.strip() == 'tag: target'
960 assert target.getchildren() == []
962 assert target.getchildren() == []
961
963
962 def test_description_is_escaped_on_index_page(self, backend, pr_util):
964 def test_description_is_escaped_on_index_page(self, backend, pr_util):
963 xss_description = "<script>alert('Hi!')</script>"
965 xss_description = "<script>alert('Hi!')</script>"
964 pull_request = pr_util.create_pull_request(description=xss_description)
966 pull_request = pr_util.create_pull_request(description=xss_description)
965 response = self.app.get(url(
967 response = self.app.get(url(
966 controller='pullrequests', action='show_all',
968 controller='pullrequests', action='show_all',
967 repo_name=pull_request.target_repo.repo_name))
969 repo_name=pull_request.target_repo.repo_name))
968 response.mustcontain(
970 response.mustcontain(
969 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
971 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
970
972
971 @pytest.mark.parametrize('mergeable', [True, False])
973 @pytest.mark.parametrize('mergeable', [True, False])
972 def test_shadow_repository_link(
974 def test_shadow_repository_link(
973 self, mergeable, pr_util, http_host_stub):
975 self, mergeable, pr_util, http_host_stub):
974 """
976 """
975 Check that the pull request summary page displays a link to the shadow
977 Check that the pull request summary page displays a link to the shadow
976 repository if the pull request is mergeable. If it is not mergeable
978 repository if the pull request is mergeable. If it is not mergeable
977 the link should not be displayed.
979 the link should not be displayed.
978 """
980 """
979 pull_request = pr_util.create_pull_request(
981 pull_request = pr_util.create_pull_request(
980 mergeable=mergeable, enable_notifications=False)
982 mergeable=mergeable, enable_notifications=False)
981 target_repo = pull_request.target_repo.scm_instance()
983 target_repo = pull_request.target_repo.scm_instance()
982 pr_id = pull_request.pull_request_id
984 pr_id = pull_request.pull_request_id
983 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
985 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
984 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
986 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
985
987
986 response = self.app.get(url(
988 response = self.app.get(url(
987 controller='pullrequests', action='show',
989 controller='pullrequests', action='show',
988 repo_name=target_repo.name,
990 repo_name=target_repo.name,
989 pull_request_id=str(pr_id)))
991 pull_request_id=str(pr_id)))
990
992
991 assertr = AssertResponse(response)
993 assertr = AssertResponse(response)
992 if mergeable:
994 if mergeable:
993 assertr.element_value_contains(
995 assertr.element_value_contains(
994 'div.pr-mergeinfo input', shadow_url)
996 'div.pr-mergeinfo input', shadow_url)
995 assertr.element_value_contains(
997 assertr.element_value_contains(
996 'div.pr-mergeinfo input', 'pr-merge')
998 'div.pr-mergeinfo input', 'pr-merge')
997 else:
999 else:
998 assertr.no_element_exists('div.pr-mergeinfo')
1000 assertr.no_element_exists('div.pr-mergeinfo')
999
1001
1000
1002
1001 @pytest.mark.usefixtures('app')
1003 @pytest.mark.usefixtures('app')
1002 @pytest.mark.backends("git", "hg")
1004 @pytest.mark.backends("git", "hg")
1003 class TestPullrequestsControllerDelete(object):
1005 class TestPullrequestsControllerDelete(object):
1004 def test_pull_request_delete_button_permissions_admin(
1006 def test_pull_request_delete_button_permissions_admin(
1005 self, autologin_user, user_admin, pr_util):
1007 self, autologin_user, user_admin, pr_util):
1006 pull_request = pr_util.create_pull_request(
1008 pull_request = pr_util.create_pull_request(
1007 author=user_admin.username, enable_notifications=False)
1009 author=user_admin.username, enable_notifications=False)
1008
1010
1009 response = self.app.get(url(
1011 response = self.app.get(url(
1010 controller='pullrequests', action='show',
1012 controller='pullrequests', action='show',
1011 repo_name=pull_request.target_repo.scm_instance().name,
1013 repo_name=pull_request.target_repo.scm_instance().name,
1012 pull_request_id=str(pull_request.pull_request_id)))
1014 pull_request_id=str(pull_request.pull_request_id)))
1013
1015
1014 response.mustcontain('id="delete_pullrequest"')
1016 response.mustcontain('id="delete_pullrequest"')
1015 response.mustcontain('Confirm to delete this pull request')
1017 response.mustcontain('Confirm to delete this pull request')
1016
1018
1017 def test_pull_request_delete_button_permissions_owner(
1019 def test_pull_request_delete_button_permissions_owner(
1018 self, autologin_regular_user, user_regular, pr_util):
1020 self, autologin_regular_user, user_regular, pr_util):
1019 pull_request = pr_util.create_pull_request(
1021 pull_request = pr_util.create_pull_request(
1020 author=user_regular.username, enable_notifications=False)
1022 author=user_regular.username, enable_notifications=False)
1021
1023
1022 response = self.app.get(url(
1024 response = self.app.get(url(
1023 controller='pullrequests', action='show',
1025 controller='pullrequests', action='show',
1024 repo_name=pull_request.target_repo.scm_instance().name,
1026 repo_name=pull_request.target_repo.scm_instance().name,
1025 pull_request_id=str(pull_request.pull_request_id)))
1027 pull_request_id=str(pull_request.pull_request_id)))
1026
1028
1027 response.mustcontain('id="delete_pullrequest"')
1029 response.mustcontain('id="delete_pullrequest"')
1028 response.mustcontain('Confirm to delete this pull request')
1030 response.mustcontain('Confirm to delete this pull request')
1029
1031
1030 def test_pull_request_delete_button_permissions_forbidden(
1032 def test_pull_request_delete_button_permissions_forbidden(
1031 self, autologin_regular_user, user_regular, user_admin, pr_util):
1033 self, autologin_regular_user, user_regular, user_admin, pr_util):
1032 pull_request = pr_util.create_pull_request(
1034 pull_request = pr_util.create_pull_request(
1033 author=user_admin.username, enable_notifications=False)
1035 author=user_admin.username, enable_notifications=False)
1034
1036
1035 response = self.app.get(url(
1037 response = self.app.get(url(
1036 controller='pullrequests', action='show',
1038 controller='pullrequests', action='show',
1037 repo_name=pull_request.target_repo.scm_instance().name,
1039 repo_name=pull_request.target_repo.scm_instance().name,
1038 pull_request_id=str(pull_request.pull_request_id)))
1040 pull_request_id=str(pull_request.pull_request_id)))
1039 response.mustcontain(no=['id="delete_pullrequest"'])
1041 response.mustcontain(no=['id="delete_pullrequest"'])
1040 response.mustcontain(no=['Confirm to delete this pull request'])
1042 response.mustcontain(no=['Confirm to delete this pull request'])
1041
1043
1042 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1044 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1043 self, autologin_regular_user, user_regular, user_admin, pr_util,
1045 self, autologin_regular_user, user_regular, user_admin, pr_util,
1044 user_util):
1046 user_util):
1045
1047
1046 pull_request = pr_util.create_pull_request(
1048 pull_request = pr_util.create_pull_request(
1047 author=user_admin.username, enable_notifications=False)
1049 author=user_admin.username, enable_notifications=False)
1048
1050
1049 user_util.grant_user_permission_to_repo(
1051 user_util.grant_user_permission_to_repo(
1050 pull_request.target_repo, user_regular,
1052 pull_request.target_repo, user_regular,
1051 'repository.write')
1053 'repository.write')
1052
1054
1053 response = self.app.get(url(
1055 response = self.app.get(url(
1054 controller='pullrequests', action='show',
1056 controller='pullrequests', action='show',
1055 repo_name=pull_request.target_repo.scm_instance().name,
1057 repo_name=pull_request.target_repo.scm_instance().name,
1056 pull_request_id=str(pull_request.pull_request_id)))
1058 pull_request_id=str(pull_request.pull_request_id)))
1057
1059
1058 response.mustcontain('id="open_edit_pullrequest"')
1060 response.mustcontain('id="open_edit_pullrequest"')
1059 response.mustcontain('id="delete_pullrequest"')
1061 response.mustcontain('id="delete_pullrequest"')
1060 response.mustcontain(no=['Confirm to delete this pull request'])
1062 response.mustcontain(no=['Confirm to delete this pull request'])
1061
1063
1062
1064
1063 def assert_pull_request_status(pull_request, expected_status):
1065 def assert_pull_request_status(pull_request, expected_status):
1064 status = ChangesetStatusModel().calculated_review_status(
1066 status = ChangesetStatusModel().calculated_review_status(
1065 pull_request=pull_request)
1067 pull_request=pull_request)
1066 assert status == expected_status
1068 assert status == expected_status
1067
1069
1068
1070
1069 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1071 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1070 @pytest.mark.usefixtures("autologin_user")
1072 @pytest.mark.usefixtures("autologin_user")
1071 def test_redirects_to_repo_summary_for_svn_repositories(
1073 def test_redirects_to_repo_summary_for_svn_repositories(
1072 backend_svn, app, action):
1074 backend_svn, app, action):
1073 denied_actions = ['show_all', 'index', 'create']
1075 denied_actions = ['show_all', 'index', 'create']
1074 for action in denied_actions:
1076 for action in denied_actions:
1075 response = app.get(url(
1077 response = app.get(url(
1076 controller='pullrequests', action=action,
1078 controller='pullrequests', action=action,
1077 repo_name=backend_svn.repo_name))
1079 repo_name=backend_svn.repo_name))
1078 assert response.status_int == 302
1080 assert response.status_int == 302
1079
1081
1080 # Not allowed, redirect to the summary
1082 # Not allowed, redirect to the summary
1081 redirected = response.follow()
1083 redirected = response.follow()
1082 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1084 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1083
1085
1084 # URL adds leading slash and path doesn't have it
1086 # URL adds leading slash and path doesn't have it
1085 assert redirected.req.path == summary_url
1087 assert redirected.req.path == summary_url
1086
1088
1087
1089
1088 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1090 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1089 # TODO: johbo: Global import not possible because models.forms blows up
1091 # TODO: johbo: Global import not possible because models.forms blows up
1090 from rhodecode.controllers.pullrequests import PullrequestsController
1092 from rhodecode.controllers.pullrequests import PullrequestsController
1091 controller = PullrequestsController()
1093 controller = PullrequestsController()
1092 patcher = mock.patch(
1094 patcher = mock.patch(
1093 'rhodecode.model.db.BaseModel.get', return_value=None)
1095 'rhodecode.model.db.BaseModel.get', return_value=None)
1094 with pytest.raises(HTTPNotFound), patcher:
1096 with pytest.raises(HTTPNotFound), patcher:
1095 controller._delete_comment(1)
1097 controller._delete_comment(1)
General Comments 0
You need to be logged in to leave comments. Login now