##// END OF EJS Templates
webhook: handle ${commit_id} variable independent from ${branch}....
marcink -
r3041:d813300a stable
parent child Browse files
Show More
@@ -1,269 +1,270 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.model.db import Integration
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.integrations import integration_type_registry
27 27
28 28
29 29 def route_path(name, **kwargs):
30 30 return {
31 31 'home': '/',
32 32 }[name].format(**kwargs)
33 33
34 34
35 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
36 admin_view):
37 """
38 Posts form data to create integration at the url given then deletes it and
39 checks if the redirect url is correct.
40 """
41 repo_name = repo.repo_name
42 repo_group_name = repo_group.group_name
43 app.post(url, params={}, status=403) # missing csrf check
44 response = app.post(url, params={'csrf_token': csrf_token})
45 assert response.status_code == 200
46 response.mustcontain('Errors exist')
47
48 scopes_destinations = [
49 ('global',
50 ADMIN_PREFIX + '/integrations'),
51 ('root-repos',
52 ADMIN_PREFIX + '/integrations'),
53 ('repo:%s' % repo_name,
54 '/%s/settings/integrations' % repo_name),
55 ('repogroup:%s' % repo_group_name,
56 '/%s/_settings/integrations' % repo_group_name),
57 ('repogroup-recursive:%s' % repo_group_name,
58 '/%s/_settings/integrations' % repo_group_name),
59 ]
60
61 for scope, destination in scopes_destinations:
62 if admin_view:
63 destination = ADMIN_PREFIX + '/integrations'
64
65 form_data = [
66 ('csrf_token', csrf_token),
67 ('__start__', 'options:mapping'),
68 ('name', 'test integration'),
69 ('scope', scope),
70 ('enabled', 'true'),
71 ('__end__', 'options:mapping'),
72 ('__start__', 'settings:mapping'),
73 ('test_int_field', '34'),
74 ('test_string_field', ''), # empty value on purpose as it's required
75 ('__end__', 'settings:mapping'),
76 ]
77 errors_response = app.post(url, form_data)
78 assert 'Errors exist' in errors_response.body
79
80 form_data[-2] = ('test_string_field', 'data!')
81 assert Session().query(Integration).count() == 0
82 created_response = app.post(url, form_data)
83 assert Session().query(Integration).count() == 1
84
85 delete_response = app.post(
86 created_response.location,
87 params={'csrf_token': csrf_token, 'delete': 'delete'})
88
89 assert Session().query(Integration).count() == 0
90 assert delete_response.location.endswith(destination)
91
92
93
35 94 @pytest.mark.usefixtures('app', 'autologin_user')
36 95 class TestIntegrationsView(object):
37 96 pass
38 97
39 98
40 99 class TestGlobalIntegrationsView(TestIntegrationsView):
41 100 def test_index_no_integrations(self):
42 101 url = ADMIN_PREFIX + '/integrations'
43 102 response = self.app.get(url)
44 103
45 104 assert response.status_code == 200
46 105 response.mustcontain('exist yet')
47 106
48 107 def test_index_with_integrations(self, global_integration_stub):
49 108 url = ADMIN_PREFIX + '/integrations'
50 109 response = self.app.get(url)
51 110
52 111 assert response.status_code == 200
53 112 response.mustcontain(no=['exist yet'])
54 113 response.mustcontain(global_integration_stub.name)
55 114
56 115 @pytest.mark.parametrize(
57 116 'IntegrationType', integration_type_registry.values())
58 117 def test_new_integration_page(self, IntegrationType):
59 118 url = ADMIN_PREFIX + '/integrations/new'
60 119
61 120 response = self.app.get(url, status=200)
62 121 if not IntegrationType.is_dummy:
63 122 url = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
64 123 integration=IntegrationType.key)
65 124 response.mustcontain(url)
66 125
67 126 @pytest.mark.parametrize(
68 127 'IntegrationType', integration_type_registry.values())
69 128 def test_get_create_integration_page(self, IntegrationType):
70 129 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
71 130 integration_key=IntegrationType.key)
72 131 if IntegrationType.is_dummy:
73 132 self.app.get(url, status=404)
74 133 else:
75 134 response = self.app.get(url, status=200)
76 135 response.mustcontain(IntegrationType.display_name)
77 136
78 137 def test_post_integration_page(self, StubIntegrationType, csrf_token,
79 138 test_repo_group, backend_random):
80 139 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
81 140 integration_key=StubIntegrationType.key)
82 141
83 142 _post_integration_test_helper(
84 143 self.app, url, csrf_token, admin_view=True,
85 144 repo=backend_random.repo, repo_group=test_repo_group)
86 145
87 146
88 147 class TestRepoIntegrationsView(TestIntegrationsView):
89 148 def test_index_no_integrations(self, backend_random):
90 149 url = '/{repo_name}/settings/integrations'.format(
91 150 repo_name=backend_random.repo.repo_name)
92 151 response = self.app.get(url)
93 152
94 153 assert response.status_code == 200
95 154 response.mustcontain('exist yet')
96 155
97 156 def test_index_with_integrations(self, repo_integration_stub):
98 157 url = '/{repo_name}/settings/integrations'.format(
99 158 repo_name=repo_integration_stub.repo.repo_name)
100 159 stub_name = repo_integration_stub.name
101 160
102 161 response = self.app.get(url)
103 162
104 163 assert response.status_code == 200
105 164 response.mustcontain(stub_name)
106 165 response.mustcontain(no=['exist yet'])
107 166
108 167 @pytest.mark.parametrize(
109 168 'IntegrationType', integration_type_registry.values())
110 169 def test_new_integration_page(self, backend_random, IntegrationType):
111 170 repo_name = backend_random.repo.repo_name
112 171 url = '/{repo_name}/settings/integrations/new'.format(
113 172 repo_name=repo_name)
114 173
115 174 response = self.app.get(url, status=200)
116 175
117 176 url = '/{repo_name}/settings/integrations/{integration}/new'.format(
118 177 repo_name=repo_name,
119 178 integration=IntegrationType.key)
120 179 if not IntegrationType.is_dummy:
121 180 response.mustcontain(url)
122 181
123 182 @pytest.mark.parametrize(
124 183 'IntegrationType', integration_type_registry.values())
125 184 def test_get_create_integration_page(self, backend_random, IntegrationType):
126 185 repo_name = backend_random.repo.repo_name
127 186 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
128 187 repo_name=repo_name, integration_key=IntegrationType.key)
129 188 if IntegrationType.is_dummy:
130 189 self.app.get(url, status=404)
131 190 else:
132 191 response = self.app.get(url, status=200)
133 192 response.mustcontain(IntegrationType.display_name)
134 193
135 194 def test_post_integration_page(self, backend_random, test_repo_group,
136 195 StubIntegrationType, csrf_token):
137 196 repo_name = backend_random.repo.repo_name
138 197 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
139 198 repo_name=repo_name, integration_key=StubIntegrationType.key)
140 199
141 200 _post_integration_test_helper(
142 201 self.app, url, csrf_token, admin_view=False,
143 202 repo=backend_random.repo, repo_group=test_repo_group)
144 203
145 204
146 205 class TestRepoGroupIntegrationsView(TestIntegrationsView):
147 206 def test_index_no_integrations(self, test_repo_group):
148 207 url = '/{repo_group_name}/_settings/integrations'.format(
149 208 repo_group_name=test_repo_group.group_name)
150 209 response = self.app.get(url)
151 210
152 211 assert response.status_code == 200
153 212 response.mustcontain('exist yet')
154 213
155 214 def test_index_with_integrations(
156 215 self, test_repo_group, repogroup_integration_stub):
157 216
158 217 url = '/{repo_group_name}/_settings/integrations'.format(
159 218 repo_group_name=test_repo_group.group_name)
160 219
161 220 stub_name = repogroup_integration_stub.name
162 221 response = self.app.get(url)
163 222
164 223 assert response.status_code == 200
165 224 response.mustcontain(no=['exist yet'])
166 225 response.mustcontain(stub_name)
167 226
168 227 def test_new_integration_page(self, test_repo_group):
169 228 repo_group_name = test_repo_group.group_name
170 229 url = '/{repo_group_name}/_settings/integrations/new'.format(
171 230 repo_group_name=test_repo_group.group_name)
172 231
173 232 response = self.app.get(url)
174 233
175 234 assert response.status_code == 200
176 235
177 236 for integration_key, integration_obj in integration_type_registry.items():
178 237 if not integration_obj.is_dummy:
179 238 nurl = (
180 239 '/{repo_group_name}/_settings/integrations/{integration}/new').format(
181 240 repo_group_name=repo_group_name,
182 241 integration=integration_key)
183 242 response.mustcontain(nurl)
184 243
185 244 @pytest.mark.parametrize(
186 245 'IntegrationType', integration_type_registry.values())
187 246 def test_get_create_integration_page(
188 247 self, test_repo_group, IntegrationType):
189 248
190 249 repo_group_name = test_repo_group.group_name
191 250 url = ('/{repo_group_name}/_settings/integrations/{integration_key}/new'
192 251 ).format(repo_group_name=repo_group_name,
193 252 integration_key=IntegrationType.key)
194 253
195 254 if not IntegrationType.is_dummy:
196 255 response = self.app.get(url, status=200)
197 256 response.mustcontain(IntegrationType.display_name)
198 257 else:
199 258 self.app.get(url, status=404)
200 259
201 260 def test_post_integration_page(self, test_repo_group, backend_random,
202 261 StubIntegrationType, csrf_token):
203 262
204 263 repo_group_name = test_repo_group.group_name
205 264 url = ('/{repo_group_name}/_settings/integrations/{integration_key}/new'
206 265 ).format(repo_group_name=repo_group_name,
207 266 integration_key=StubIntegrationType.key)
208 267
209 268 _post_integration_test_helper(
210 269 self.app, url, csrf_token, admin_view=False,
211 270 repo=backend_random.repo, repo_group=test_repo_group)
212
213
214 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
215 admin_view):
216 """
217 Posts form data to create integration at the url given then deletes it and
218 checks if the redirect url is correct.
219 """
220 repo_name = repo.repo_name
221 repo_group_name = repo_group.group_name
222 app.post(url, params={}, status=403) # missing csrf check
223 response = app.post(url, params={'csrf_token': csrf_token})
224 assert response.status_code == 200
225 response.mustcontain('Errors exist')
226
227 scopes_destinations = [
228 ('global',
229 ADMIN_PREFIX + '/integrations'),
230 ('root-repos',
231 ADMIN_PREFIX + '/integrations'),
232 ('repo:%s' % repo_name,
233 '/%s/settings/integrations' % repo_name),
234 ('repogroup:%s' % repo_group_name,
235 '/%s/_settings/integrations' % repo_group_name),
236 ('repogroup-recursive:%s' % repo_group_name,
237 '/%s/_settings/integrations' % repo_group_name),
238 ]
239
240 for scope, destination in scopes_destinations:
241 if admin_view:
242 destination = ADMIN_PREFIX + '/integrations'
243
244 form_data = [
245 ('csrf_token', csrf_token),
246 ('__start__', 'options:mapping'),
247 ('name', 'test integration'),
248 ('scope', scope),
249 ('enabled', 'true'),
250 ('__end__', 'options:mapping'),
251 ('__start__', 'settings:mapping'),
252 ('test_int_field', '34'),
253 ('test_string_field', ''), # empty value on purpose as it's required
254 ('__end__', 'settings:mapping'),
255 ]
256 errors_response = app.post(url, form_data)
257 assert 'Errors exist' in errors_response.body
258
259 form_data[-2] = ('test_string_field', 'data!')
260 assert Session().query(Integration).count() == 0
261 created_response = app.post(url, form_data)
262 assert Session().query(Integration).count() == 1
263
264 delete_response = app.post(
265 created_response.location,
266 params={'csrf_token': csrf_token, 'delete': 'delete'})
267
268 assert Session().query(Integration).count() == 0
269 assert delete_response.location.endswith(destination)
@@ -1,322 +1,322 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import string
23 23 import collections
24 24 import logging
25 25
26 26 from mako import exceptions
27 27
28 28 from rhodecode.translation import _
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class IntegrationTypeBase(object):
35 35 """ Base class for IntegrationType plugins """
36 36 is_dummy = False
37 37 description = ''
38 38
39 39 @classmethod
40 40 def icon(cls):
41 41 return '''
42 42 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
43 43 <svg
44 44 xmlns:dc="http://purl.org/dc/elements/1.1/"
45 45 xmlns:cc="http://creativecommons.org/ns#"
46 46 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
47 47 xmlns:svg="http://www.w3.org/2000/svg"
48 48 xmlns="http://www.w3.org/2000/svg"
49 49 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
50 50 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
51 51 viewBox="0 -256 1792 1792"
52 52 id="svg3025"
53 53 version="1.1"
54 54 inkscape:version="0.48.3.1 r9886"
55 55 width="100%"
56 56 height="100%"
57 57 sodipodi:docname="cog_font_awesome.svg">
58 58 <metadata
59 59 id="metadata3035">
60 60 <rdf:RDF>
61 61 <cc:Work
62 62 rdf:about="">
63 63 <dc:format>image/svg+xml</dc:format>
64 64 <dc:type
65 65 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
66 66 </cc:Work>
67 67 </rdf:RDF>
68 68 </metadata>
69 69 <defs
70 70 id="defs3033" />
71 71 <sodipodi:namedview
72 72 pagecolor="#ffffff"
73 73 bordercolor="#666666"
74 74 borderopacity="1"
75 75 objecttolerance="10"
76 76 gridtolerance="10"
77 77 guidetolerance="10"
78 78 inkscape:pageopacity="0"
79 79 inkscape:pageshadow="2"
80 80 inkscape:window-width="640"
81 81 inkscape:window-height="480"
82 82 id="namedview3031"
83 83 showgrid="false"
84 84 inkscape:zoom="0.13169643"
85 85 inkscape:cx="896"
86 86 inkscape:cy="896"
87 87 inkscape:window-x="0"
88 88 inkscape:window-y="25"
89 89 inkscape:window-maximized="0"
90 90 inkscape:current-layer="svg3025" />
91 91 <g
92 92 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
93 93 id="g3027">
94 94 <path
95 95 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
96 96 id="path3029"
97 97 inkscape:connector-curvature="0"
98 98 style="fill:currentColor" />
99 99 </g>
100 100 </svg>
101 101 '''
102 102
103 103 def __init__(self, settings):
104 104 """
105 105 :param settings: dict of settings to be used for the integration
106 106 """
107 107 self.settings = settings
108 108
109 109 def settings_schema(self):
110 110 """
111 111 A colander schema of settings for the integration type
112 112 """
113 113 return colander.Schema()
114 114
115 115
116 116 class EEIntegration(IntegrationTypeBase):
117 117 description = 'Integration available in RhodeCode EE edition.'
118 118 is_dummy = True
119 119
120 120 def __init__(self, name, key, settings=None):
121 121 self.display_name = name
122 122 self.key = key
123 123 super(EEIntegration, self).__init__(settings)
124 124
125 125
126 126 # Helpers #
127 127 # updating this required to update the `common_vars` as well.
128 128 WEBHOOK_URL_VARS = [
129 129 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
130 130 ('repo_name', 'Full name of the repository'),
131 131 ('repo_type', 'VCS type of repository'),
132 132 ('repo_id', 'Unique id of repository'),
133 133 ('repo_url', 'Repository url'),
134 134 # extra repo fields
135 135 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
136 136
137 137 # special attrs below that we handle, using multi-call
138 138 ('branch', 'Name of each branch submitted, if any.'),
139 139 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
140 140 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
141 141
142 142 # pr events vars
143 143 ('pull_request_id', 'Unique ID of the pull request.'),
144 144 ('pull_request_title', 'Title of the pull request.'),
145 145 ('pull_request_url', 'Pull request url.'),
146 146 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
147 147 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
148 148 'Changes after PR update'),
149 149
150 150 # user who triggers the call
151 151 ('username', 'User who triggered the call.'),
152 152 ('user_id', 'User id who triggered the call.'),
153 153 ]
154 154
155 155 # common vars for url template used for CI plugins. Shared with webhook
156 156 CI_URL_VARS = WEBHOOK_URL_VARS
157 157
158 158
159 159 class CommitParsingDataHandler(object):
160 160
161 161 def aggregate_branch_data(self, branches, commits):
162 162 branch_data = collections.OrderedDict()
163 163 for obj in branches:
164 164 branch_data[obj['name']] = obj
165 165
166 166 branches_commits = collections.OrderedDict()
167 167 for commit in commits:
168 168 if commit.get('git_ref_change'):
169 169 # special case for GIT that allows creating tags,
170 170 # deleting branches without associated commit
171 171 continue
172 172 commit_branch = commit['branch']
173 173
174 174 if commit_branch not in branches_commits:
175 175 _branch = branch_data[commit_branch] \
176 176 if commit_branch else commit_branch
177 177 branch_commits = {'branch': _branch,
178 178 'branch_head': '',
179 179 'commits': []}
180 180 branches_commits[commit_branch] = branch_commits
181 181
182 182 branch_commits = branches_commits[commit_branch]
183 183 branch_commits['commits'].append(commit)
184 184 branch_commits['branch_head'] = commit['raw_id']
185 185 return branches_commits
186 186
187 187
188 188 class WebhookDataHandler(CommitParsingDataHandler):
189 189 name = 'webhook'
190 190
191 191 def __init__(self, template_url, headers):
192 192 self.template_url = template_url
193 193 self.headers = headers
194 194
195 195 def get_base_parsed_template(self, data):
196 196 """
197 197 initially parses the passed in template with some common variables
198 198 available on ALL calls
199 199 """
200 200 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
201 201 common_vars = {
202 202 'repo_name': data['repo']['repo_name'],
203 203 'repo_type': data['repo']['repo_type'],
204 204 'repo_id': data['repo']['repo_id'],
205 205 'repo_url': data['repo']['url'],
206 206 'username': data['actor']['username'],
207 207 'user_id': data['actor']['user_id'],
208 208 'event_name': data['name']
209 209 }
210 210
211 211 extra_vars = {}
212 212 for extra_key, extra_val in data['repo']['extra_fields'].items():
213 213 extra_vars['extra__{}'.format(extra_key)] = extra_val
214 214 common_vars.update(extra_vars)
215 215
216 216 template_url = self.template_url.replace('${extra:', '${extra__')
217 217 return string.Template(template_url).safe_substitute(**common_vars)
218 218
219 219 def repo_push_event_handler(self, event, data):
220 220 url = self.get_base_parsed_template(data)
221 url_cals = []
221 url_calls = []
222 222
223 223 branches_commits = self.aggregate_branch_data(
224 224 data['push']['branches'], data['push']['commits'])
225 if '${branch}' in url or '${branch_head}' in url:
225 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
226 226 # call it multiple times, for each branch if used in variables
227 227 for branch, commit_ids in branches_commits.items():
228 228 branch_url = string.Template(url).safe_substitute(branch=branch)
229 229
230 230 if '${branch_head}' in branch_url:
231 231 # last commit in the aggregate is the head of the branch
232 232 branch_head = commit_ids['branch_head']
233 233 branch_url = string.Template(branch_url).safe_substitute(
234 234 branch_head=branch_head)
235 235
236 236 # call further down for each commit if used
237 237 if '${commit_id}' in branch_url:
238 238 for commit_data in commit_ids['commits']:
239 239 commit_id = commit_data['raw_id']
240 240 commit_url = string.Template(branch_url).safe_substitute(
241 241 commit_id=commit_id)
242 242 # register per-commit call
243 243 log.debug(
244 244 'register %s call(%s) to url %s',
245 245 self.name, event, commit_url)
246 url_cals.append(
246 url_calls.append(
247 247 (commit_url, self.headers, data))
248 248
249 249 else:
250 250 # register per-branch call
251 251 log.debug(
252 252 'register %s call(%s) to url %s',
253 253 self.name, event, branch_url)
254 url_cals.append(
254 url_calls.append(
255 255 (branch_url, self.headers, data))
256 256
257 257 else:
258 258 log.debug(
259 259 'register %s call(%s) to url %s', self.name, event, url)
260 url_cals.append((url, self.headers, data))
260 url_calls.append((url, self.headers, data))
261 261
262 return url_cals
262 return url_calls
263 263
264 264 def repo_create_event_handler(self, event, data):
265 265 url = self.get_base_parsed_template(data)
266 266 log.debug(
267 267 'register %s call(%s) to url %s', self.name, event, url)
268 268 return [(url, self.headers, data)]
269 269
270 270 def pull_request_event_handler(self, event, data):
271 271 url = self.get_base_parsed_template(data)
272 272 log.debug(
273 273 'register %s call(%s) to url %s', self.name, event, url)
274 274 url = string.Template(url).safe_substitute(
275 275 pull_request_id=data['pullrequest']['pull_request_id'],
276 276 pull_request_title=data['pullrequest']['title'],
277 277 pull_request_url=data['pullrequest']['url'],
278 278 pull_request_shadow_url=data['pullrequest']['shadow_url'],
279 279 pull_request_commits_uid=data['pullrequest']['commits_uid'],
280 280 )
281 281 return [(url, self.headers, data)]
282 282
283 283 def __call__(self, event, data):
284 284 from rhodecode import events
285 285
286 286 if isinstance(event, events.RepoPushEvent):
287 287 return self.repo_push_event_handler(event, data)
288 288 elif isinstance(event, events.RepoCreateEvent):
289 289 return self.repo_create_event_handler(event, data)
290 290 elif isinstance(event, events.PullRequestEvent):
291 291 return self.pull_request_event_handler(event, data)
292 292 else:
293 293 raise ValueError(
294 294 'event type `%s` not in supported list: %s' % (
295 295 event.__class__, events))
296 296
297 297
298 298 def get_auth(settings):
299 299 from requests.auth import HTTPBasicAuth
300 300 username = settings.get('username')
301 301 password = settings.get('password')
302 302 if username and password:
303 303 return HTTPBasicAuth(username, password)
304 304 return None
305 305
306 306
307 307 def get_web_token(settings):
308 308 return settings['secret_token']
309 309
310 310
311 311 def get_url_vars(url_vars):
312 312 return '\n'.join(
313 313 '{} - {}'.format('${' + key + '}', explanation)
314 314 for key, explanation in url_vars)
315 315
316 316
317 317 def render_with_traceback(template, *args, **kwargs):
318 318 try:
319 319 return template.render(*args, **kwargs)
320 320 except Exception:
321 321 log.error(exceptions.text_error_template().render())
322 322 raise
@@ -1,124 +1,132 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode import events
24 24 from rhodecode.lib.utils2 import AttributeDict
25 25 from rhodecode.integrations.types.webhook import WebhookDataHandler
26 26
27 27
28 28 @pytest.fixture
29 29 def base_data():
30 30 return {
31 31 'name': 'event',
32 32 'repo': {
33 33 'repo_name': 'foo',
34 34 'repo_type': 'hg',
35 35 'repo_id': '12',
36 36 'url': 'http://repo.url/foo',
37 37 'extra_fields': {},
38 38 },
39 39 'actor': {
40 40 'username': 'actor_name',
41 41 'user_id': 1
42 42 }
43 43 }
44 44
45 45
46 46 def test_webhook_parse_url_invalid_event():
47 47 template_url = 'http://server.com/${repo_name}/build'
48 48 handler = WebhookDataHandler(
49 49 template_url, {'exmaple-header': 'header-values'})
50 50 event = events.RepoDeleteEvent('')
51 51 with pytest.raises(ValueError) as err:
52 52 handler(event, {})
53 53
54 54 err = str(err.value)
55 55 assert err.startswith(
56 56 'event type `%s` not in supported list' % event.__class__)
57 57
58 58
59 59 @pytest.mark.parametrize('template,expected_urls', [
60 60 ('http://server.com/${repo_name}/build',
61 61 ['http://server.com/foo/build']),
62 62 ('http://server.com/${repo_name}/${repo_type}',
63 63 ['http://server.com/foo/hg']),
64 64 ('http://${server}.com/${repo_name}/${repo_id}',
65 65 ['http://${server}.com/foo/12']),
66 66 ('http://server.com/${branch}/build',
67 67 ['http://server.com/${branch}/build']),
68 68 ])
69 69 def test_webook_parse_url_for_create_event(base_data, template, expected_urls):
70 70 headers = {'exmaple-header': 'header-values'}
71 71 handler = WebhookDataHandler(template, headers)
72 72 urls = handler(events.RepoCreateEvent(''), base_data)
73 73 assert urls == [
74 74 (url, headers, base_data) for url in expected_urls]
75 75
76 76
77 77 @pytest.mark.parametrize('template,expected_urls', [
78 78 ('http://server.com/${repo_name}/${pull_request_id}',
79 79 ['http://server.com/foo/999']),
80 80 ('http://server.com/${repo_name}/${pull_request_url}',
81 81 ['http://server.com/foo/http://pr-url.com']),
82 82 ])
83 83 def test_webook_parse_url_for_pull_request_event(
84 84 base_data, template, expected_urls):
85 85
86 86 base_data['pullrequest'] = {
87 87 'pull_request_id': 999,
88 88 'url': 'http://pr-url.com',
89 89 'title': 'example-pr-title',
90 90 'commits_uid': 'abcdefg1234',
91 91 'shadow_url': 'http://pr-url.com/repository'
92 92 }
93 93 headers = {'exmaple-header': 'header-values'}
94 94 handler = WebhookDataHandler(template, headers)
95 95 urls = handler(events.PullRequestCreateEvent(
96 96 AttributeDict({'target_repo': 'foo'})), base_data)
97 97 assert urls == [
98 98 (url, headers, base_data) for url in expected_urls]
99 99
100 100
101 101 @pytest.mark.parametrize('template,expected_urls', [
102 102 ('http://server.com/${branch}/build',
103 103 ['http://server.com/stable/build',
104 104 'http://server.com/dev/build']),
105 105 ('http://server.com/${branch}/${commit_id}',
106 106 ['http://server.com/stable/stable-xxx',
107 107 'http://server.com/stable/stable-yyy',
108 108 'http://server.com/dev/dev-xxx',
109 109 'http://server.com/dev/dev-yyy']),
110 ('http://server.com/${branch_head}',
111 ['http://server.com/stable-yyy',
112 'http://server.com/dev-yyy']),
113 ('http://server.com/${commit_id}',
114 ['http://server.com/stable-xxx',
115 'http://server.com/stable-yyy',
116 'http://server.com/dev-xxx',
117 'http://server.com/dev-yyy']),
110 118 ])
111 119 def test_webook_parse_url_for_push_event(
112 120 baseapp, repo_push_event, base_data, template, expected_urls):
113 121 base_data['push'] = {
114 122 'branches': [{'name': 'stable'}, {'name': 'dev'}],
115 123 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
116 124 {'branch': 'stable', 'raw_id': 'stable-yyy'},
117 125 {'branch': 'dev', 'raw_id': 'dev-xxx'},
118 126 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
119 127 }
120 128 headers = {'exmaple-header': 'header-values'}
121 129 handler = WebhookDataHandler(template, headers)
122 130 urls = handler(repo_push_event, base_data)
123 131 assert urls == [
124 132 (url, headers, base_data) for url in expected_urls]
General Comments 0
You need to be logged in to leave comments. Login now