Merge lp:~wgrant/launchpad/webhook-event-types into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 17687
Proposed branch: lp:~wgrant/launchpad/webhook-event-types
Merge into: lp:launchpad
Diff against target: 334 lines (+96/-20)
7 files modified
lib/lp/services/webhooks/browser.py (+7/-4)
lib/lp/services/webhooks/configure.zcml (+9/-0)
lib/lp/services/webhooks/interfaces.py (+18/-3)
lib/lp/services/webhooks/model.py (+2/-0)
lib/lp/services/webhooks/tests/test_browser.py (+6/-5)
lib/lp/services/webhooks/tests/test_model.py (+19/-0)
lib/lp/services/webhooks/tests/test_webservice.py (+35/-8)
To merge this branch: bzr merge lp:~wgrant/launchpad/webhook-event-types
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+267470@code.launchpad.net

Commit message

Improve Webhook.event_types widget and value checking.

Description of the change

Improve Webhook.event_types widget and value checking.

Rather than a weird Zopey text list widget, there's now a set of checkboxes for each known event type. I've eschewed a normal enum due to the non-DB-column and potentially fluid nature of event types; a custom vocabulary is easy and makes migration easier when we change things later.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/services/webhooks/browser.py'
2--- lib/lp/services/webhooks/browser.py 2015-08-04 13:18:24 +0000
3+++ lib/lp/services/webhooks/browser.py 2015-08-10 06:17:10 +0000
4@@ -16,9 +16,11 @@
5
6 from lp.app.browser.launchpadform import (
7 action,
8+ custom_widget,
9 LaunchpadEditFormView,
10 LaunchpadFormView,
11 )
12+from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
13 from lp.services.propertycache import cachedproperty
14 from lp.services.webapp import (
15 canonical_url,
16@@ -103,8 +105,7 @@
17
18
19 class WebhookEditSchema(Interface):
20- # XXX wgrant 2015-08-04: Need custom widgets for secret and
21- # event_types.
22+ # XXX wgrant 2015-08-04: Need custom widget for secret.
23 use_template(IWebhook, include=['delivery_url', 'event_types', 'active'])
24
25
26@@ -113,6 +114,7 @@
27 page_title = label = "Add webhook"
28
29 schema = WebhookEditSchema
30+ custom_widget('event_types', LabeledMultiCheckBoxWidget)
31
32 @property
33 def inside_breadcrumb(self):
34@@ -136,10 +138,11 @@
35
36 class WebhookView(LaunchpadEditFormView):
37
38- schema = WebhookEditSchema
39-
40 label = "Manage webhook"
41
42+ schema = WebhookEditSchema
43+ custom_widget('event_types', LabeledMultiCheckBoxWidget)
44+
45 @property
46 def next_url(self):
47 # The edit form is the default view, so the URL doesn't need the
48
49=== modified file 'lib/lp/services/webhooks/configure.zcml'
50--- lib/lp/services/webhooks/configure.zcml 2015-08-04 11:02:02 +0000
51+++ lib/lp/services/webhooks/configure.zcml 2015-08-10 06:17:10 +0000
52@@ -21,6 +21,15 @@
53 provides="lp.services.webhooks.interfaces.IWebhookSource">
54 <allow interface="lp.services.webhooks.interfaces.IWebhookSource"/>
55 </securedutility>
56+ <securedutility
57+ name="WebhookEventType"
58+ component="lp.services.webhooks.interfaces.WebhookEventTypeVocabulary"
59+ provides="zope.schema.interfaces.IVocabularyFactory">
60+ <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
61+ </securedutility>
62+ <class class="lp.services.webhooks.interfaces.WebhookEventTypeVocabulary">
63+ <allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
64+ </class>
65
66 <securedutility
67 component="lp.services.webhooks.model.WebhookJob"
68
69=== modified file 'lib/lp/services/webhooks/interfaces.py'
70--- lib/lp/services/webhooks/interfaces.py 2015-08-04 13:59:25 +0000
71+++ lib/lp/services/webhooks/interfaces.py 2015-08-10 06:17:10 +0000
72@@ -17,6 +17,7 @@
73 'WebhookDeliveryFailure',
74 'WebhookDeliveryRetry',
75 'WebhookFeatureDisabled',
76+ 'WebhookEventTypeVocabulary',
77 ]
78
79 import httplib
80@@ -45,12 +46,14 @@
81 )
82 from zope.schema import (
83 Bool,
84+ Choice,
85 Datetime,
86 Dict,
87 Int,
88 List,
89 TextLine,
90 )
91+from zope.schema.vocabulary import SimpleVocabulary
92
93 from lp import _
94 from lp.registry.interfaces.person import IPerson
95@@ -67,6 +70,11 @@
96 )
97
98
99+WEBHOOK_EVENT_TYPES = {
100+ "git:push:0.1": "Git push",
101+ }
102+
103+
104 @error_status(httplib.UNAUTHORIZED)
105 class WebhookFeatureDisabled(Exception):
106 """Only certain users can create new Git repositories."""
107@@ -86,6 +94,15 @@
108 pass
109
110
111+class WebhookEventTypeVocabulary(SimpleVocabulary):
112+
113+ def __init__(self, context):
114+ terms = [
115+ self.createTerm(key, key, value)
116+ for key, value in WEBHOOK_EVENT_TYPES.iteritems()]
117+ super(WebhookEventTypeVocabulary, self).__init__(terms)
118+
119+
120 class IWebhook(Interface):
121
122 export_as_webservice_entry(as_of='beta')
123@@ -97,9 +114,7 @@
124 required=True, readonly=True,
125 description=_("The object for which this webhook receives events.")))
126 event_types = exported(List(
127- TextLine(), title=_("Event types"),
128- description=_(
129- "The event types for which this webhook receives events."),
130+ Choice(vocabulary='WebhookEventType'), title=_("Event types"),
131 required=True, readonly=False))
132 registrant = exported(Reference(
133 title=_("Registrant"), schema=IPerson, required=True, readonly=True,
134
135=== modified file 'lib/lp/services/webhooks/model.py'
136--- lib/lp/services/webhooks/model.py 2015-08-04 13:59:25 +0000
137+++ lib/lp/services/webhooks/model.py 2015-08-10 06:17:10 +0000
138@@ -140,6 +140,8 @@
139 @event_types.setter
140 def event_types(self, event_types):
141 updated_data = self.json_data or {}
142+ # The correctness of the values is also checked by zope.schema,
143+ # but best to be safe.
144 assert isinstance(event_types, (list, tuple))
145 assert all(isinstance(v, basestring) for v in event_types)
146 updated_data['event_types'] = event_types
147
148=== modified file 'lib/lp/services/webhooks/tests/test_browser.py'
149--- lib/lp/services/webhooks/tests/test_browser.py 2015-08-04 13:18:24 +0000
150+++ lib/lp/services/webhooks/tests/test_browser.py 2015-08-10 06:17:10 +0000
151@@ -149,7 +149,8 @@
152 "+new-webhook", method="POST",
153 form={
154 "field.delivery_url": "http://example.com/test",
155- "field.active": "on", "field.event_types.count": "0",
156+ "field.active": "on", "field.event_types-empty-marker": "1",
157+ "field.event_types": "git:push:0.1",
158 "field.actions.new": "Add webhook"})
159 self.assertEqual([], view.errors)
160 hook = self.target.webhooks.one()
161@@ -160,7 +161,7 @@
162 registrant=self.owner,
163 delivery_url="http://example.com/test",
164 active=True,
165- event_types=[]))
166+ event_types=["git:push:0.1"]))
167
168 def test_rejects_bad_scheme(self):
169 transaction.commit()
170@@ -168,7 +169,7 @@
171 "+new-webhook", method="POST",
172 form={
173 "field.delivery_url": "ftp://example.com/test",
174- "field.active": "on", "field.event_types.count": "0",
175+ "field.active": "on", "field.event_types-empty-marker": "1",
176 "field.actions.new": "Add webhook"})
177 self.assertEqual(
178 ['delivery_url'], [error.field_name for error in view.errors])
179@@ -220,7 +221,7 @@
180 "+index", method="POST",
181 form={
182 "field.delivery_url": "http://example.com/edited",
183- "field.active": "off", "field.event_types.count": "0",
184+ "field.active": "off", "field.event_types-empty-marker": "1",
185 "field.actions.save": "Save webhook"})
186 self.assertEqual([], view.errors)
187 self.assertThat(
188@@ -236,7 +237,7 @@
189 "+index", method="POST",
190 form={
191 "field.delivery_url": "ftp://example.com/edited",
192- "field.active": "off", "field.event_types.count": "0",
193+ "field.active": "off", "field.event_types-empty-marker": "1",
194 "field.actions.save": "Save webhook"})
195 self.assertEqual(
196 ['delivery_url'], [error.field_name for error in view.errors])
197
198=== modified file 'lib/lp/services/webhooks/tests/test_model.py'
199--- lib/lp/services/webhooks/tests/test_model.py 2015-08-04 05:46:54 +0000
200+++ lib/lp/services/webhooks/tests/test_model.py 2015-08-10 06:17:10 +0000
201@@ -22,6 +22,7 @@
202 from lp.testing import (
203 admin_logged_in,
204 anonymous_logged_in,
205+ ExpectedException,
206 login_person,
207 person_logged_in,
208 StormStatementRecorder,
209@@ -49,6 +50,24 @@
210 webhook.date_last_modified,
211 GreaterThan(old_mtime))
212
213+ def test_event_types(self):
214+ # Webhook.event_types is a list of event type strings.
215+ webhook = self.factory.makeWebhook()
216+ with admin_logged_in():
217+ self.assertContentEqual([], webhook.event_types)
218+ webhook.event_types = ['foo', 'bar']
219+ self.assertContentEqual(['foo', 'bar'], webhook.event_types)
220+
221+ def test_event_types_bad_structure(self):
222+ # It's not possible to set Webhook.event_types to a list of the
223+ # wrong type.
224+ webhook = self.factory.makeWebhook()
225+ with admin_logged_in():
226+ self.assertContentEqual([], webhook.event_types)
227+ with ExpectedException(AssertionError, '.*'):
228+ webhook.event_types = ['foo', [1]]
229+ self.assertContentEqual([], webhook.event_types)
230+
231
232 class TestWebhookPermissions(TestCaseWithFactory):
233
234
235=== modified file 'lib/lp/services/webhooks/tests/test_webservice.py'
236--- lib/lp/services/webhooks/tests/test_webservice.py 2015-08-04 05:44:42 +0000
237+++ lib/lp/services/webhooks/tests/test_webservice.py 2015-08-10 06:17:10 +0000
238@@ -16,6 +16,7 @@
239 Is,
240 KeysEqual,
241 MatchesAll,
242+ MatchesStructure,
243 Not,
244 )
245 from zope.security.proxy import removeSecurityProxy
246@@ -73,7 +74,7 @@
247 old_mtime = representation['date_last_modified']
248 patch = json.dumps(
249 {'active': False, 'delivery_url': 'http://example.com/ep2',
250- 'event_types': ['foo', 'bar']})
251+ 'event_types': ['git:push:0.1']})
252 self.webservice.patch(
253 self.webhook_url, 'application/json', patch, api_version='devel')
254 representation = self.webservice.get(
255@@ -84,7 +85,33 @@
256 {'active': Equals(False),
257 'delivery_url': Equals('http://example.com/ep2'),
258 'date_last_modified': GreaterThan(old_mtime),
259- 'event_types': Equals(['foo', 'bar'])}))
260+ 'event_types': Equals(['git:push:0.1'])}))
261+
262+ def test_patch_event_types(self):
263+ representation = self.webservice.get(
264+ self.webhook_url, api_version='devel').jsonBody()
265+ self.assertThat(
266+ representation, ContainsDict({'event_types': Equals([])}))
267+
268+ # Including a valid type in event_types works.
269+ response = self.webservice.patch(
270+ self.webhook_url, 'application/json',
271+ json.dumps({'event_types': ['git:push:0.1']}), api_version='devel')
272+ self.assertEqual(209, response.status)
273+ representation = self.webservice.get(
274+ self.webhook_url, api_version='devel').jsonBody()
275+ self.assertThat(
276+ representation,
277+ ContainsDict({'event_types': Equals(['git:push:0.1'])}))
278+
279+ # But an unknown type is rejected.
280+ response = self.webservice.patch(
281+ self.webhook_url, 'application/json',
282+ json.dumps({'event_types': ['hg:push:0.1']}), api_version='devel')
283+ self.assertThat(response,
284+ MatchesStructure.byEquality(
285+ status=400,
286+ body="event_types: u'hg:push:0.1' isn't a valid token"))
287
288 def test_anon_forbidden(self):
289 response = LaunchpadWebServiceCaller().get(
290@@ -279,14 +306,14 @@
291 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
292 response = self.webservice.named_post(
293 self.target_url, 'newWebhook',
294- delivery_url='http://example.com/ep', event_types=['foo', 'bar'],
295- api_version='devel')
296+ delivery_url='http://example.com/ep',
297+ event_types=['git:push:0.1'], api_version='devel')
298 self.assertEqual(201, response.status)
299
300 representation = self.webservice.get(
301 self.target_url + '/webhooks', api_version='devel').jsonBody()
302 self.assertContentEqual(
303- [('http://example.com/ep', ['foo', 'bar'], True)],
304+ [('http://example.com/ep', ['git:push:0.1'], True)],
305 [(entry['delivery_url'], entry['event_types'], entry['active'])
306 for entry in representation['entries']])
307
308@@ -294,7 +321,7 @@
309 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
310 response = self.webservice.named_post(
311 self.target_url, 'newWebhook',
312- delivery_url='http://example.com/ep', event_types=['foo', 'bar'],
313+ delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
314 secret='sekrit', api_version='devel')
315 self.assertEqual(201, response.status)
316
317@@ -310,7 +337,7 @@
318 webservice = LaunchpadWebServiceCaller()
319 response = webservice.named_post(
320 self.target_url, 'newWebhook',
321- delivery_url='http://example.com/ep', event_types=['foo', 'bar'],
322+ delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
323 api_version='devel')
324 self.assertEqual(401, response.status)
325 self.assertIn('launchpad.Edit', response.body)
326@@ -318,7 +345,7 @@
327 def test_newWebhook_feature_flag_guard(self):
328 response = self.webservice.named_post(
329 self.target_url, 'newWebhook',
330- delivery_url='http://example.com/ep', event_types=['foo', 'bar'],
331+ delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
332 api_version='devel')
333 self.assertEqual(401, response.status)
334 self.assertEqual(