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
=== modified file 'lib/lp/services/webhooks/browser.py'
--- lib/lp/services/webhooks/browser.py 2015-08-04 13:18:24 +0000
+++ lib/lp/services/webhooks/browser.py 2015-08-10 06:17:10 +0000
@@ -16,9 +16,11 @@
1616
17from lp.app.browser.launchpadform import (17from lp.app.browser.launchpadform import (
18 action,18 action,
19 custom_widget,
19 LaunchpadEditFormView,20 LaunchpadEditFormView,
20 LaunchpadFormView,21 LaunchpadFormView,
21 )22 )
23from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
22from lp.services.propertycache import cachedproperty24from lp.services.propertycache import cachedproperty
23from lp.services.webapp import (25from lp.services.webapp import (
24 canonical_url,26 canonical_url,
@@ -103,8 +105,7 @@
103105
104106
105class WebhookEditSchema(Interface):107class WebhookEditSchema(Interface):
106 # XXX wgrant 2015-08-04: Need custom widgets for secret and108 # XXX wgrant 2015-08-04: Need custom widget for secret.
107 # event_types.
108 use_template(IWebhook, include=['delivery_url', 'event_types', 'active'])109 use_template(IWebhook, include=['delivery_url', 'event_types', 'active'])
109110
110111
@@ -113,6 +114,7 @@
113 page_title = label = "Add webhook"114 page_title = label = "Add webhook"
114115
115 schema = WebhookEditSchema116 schema = WebhookEditSchema
117 custom_widget('event_types', LabeledMultiCheckBoxWidget)
116118
117 @property119 @property
118 def inside_breadcrumb(self):120 def inside_breadcrumb(self):
@@ -136,10 +138,11 @@
136138
137class WebhookView(LaunchpadEditFormView):139class WebhookView(LaunchpadEditFormView):
138140
139 schema = WebhookEditSchema
140
141 label = "Manage webhook"141 label = "Manage webhook"
142142
143 schema = WebhookEditSchema
144 custom_widget('event_types', LabeledMultiCheckBoxWidget)
145
143 @property146 @property
144 def next_url(self):147 def next_url(self):
145 # The edit form is the default view, so the URL doesn't need the148 # The edit form is the default view, so the URL doesn't need the
146149
=== modified file 'lib/lp/services/webhooks/configure.zcml'
--- lib/lp/services/webhooks/configure.zcml 2015-08-04 11:02:02 +0000
+++ lib/lp/services/webhooks/configure.zcml 2015-08-10 06:17:10 +0000
@@ -21,6 +21,15 @@
21 provides="lp.services.webhooks.interfaces.IWebhookSource">21 provides="lp.services.webhooks.interfaces.IWebhookSource">
22 <allow interface="lp.services.webhooks.interfaces.IWebhookSource"/>22 <allow interface="lp.services.webhooks.interfaces.IWebhookSource"/>
23 </securedutility>23 </securedutility>
24 <securedutility
25 name="WebhookEventType"
26 component="lp.services.webhooks.interfaces.WebhookEventTypeVocabulary"
27 provides="zope.schema.interfaces.IVocabularyFactory">
28 <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
29 </securedutility>
30 <class class="lp.services.webhooks.interfaces.WebhookEventTypeVocabulary">
31 <allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
32 </class>
2433
25 <securedutility34 <securedutility
26 component="lp.services.webhooks.model.WebhookJob"35 component="lp.services.webhooks.model.WebhookJob"
2736
=== modified file 'lib/lp/services/webhooks/interfaces.py'
--- lib/lp/services/webhooks/interfaces.py 2015-08-04 13:59:25 +0000
+++ lib/lp/services/webhooks/interfaces.py 2015-08-10 06:17:10 +0000
@@ -17,6 +17,7 @@
17 'WebhookDeliveryFailure',17 'WebhookDeliveryFailure',
18 'WebhookDeliveryRetry',18 'WebhookDeliveryRetry',
19 'WebhookFeatureDisabled',19 'WebhookFeatureDisabled',
20 'WebhookEventTypeVocabulary',
20 ]21 ]
2122
22import httplib23import httplib
@@ -45,12 +46,14 @@
45 )46 )
46from zope.schema import (47from zope.schema import (
47 Bool,48 Bool,
49 Choice,
48 Datetime,50 Datetime,
49 Dict,51 Dict,
50 Int,52 Int,
51 List,53 List,
52 TextLine,54 TextLine,
53 )55 )
56from zope.schema.vocabulary import SimpleVocabulary
5457
55from lp import _58from lp import _
56from lp.registry.interfaces.person import IPerson59from lp.registry.interfaces.person import IPerson
@@ -67,6 +70,11 @@
67 )70 )
6871
6972
73WEBHOOK_EVENT_TYPES = {
74 "git:push:0.1": "Git push",
75 }
76
77
70@error_status(httplib.UNAUTHORIZED)78@error_status(httplib.UNAUTHORIZED)
71class WebhookFeatureDisabled(Exception):79class WebhookFeatureDisabled(Exception):
72 """Only certain users can create new Git repositories."""80 """Only certain users can create new Git repositories."""
@@ -86,6 +94,15 @@
86 pass94 pass
8795
8896
97class WebhookEventTypeVocabulary(SimpleVocabulary):
98
99 def __init__(self, context):
100 terms = [
101 self.createTerm(key, key, value)
102 for key, value in WEBHOOK_EVENT_TYPES.iteritems()]
103 super(WebhookEventTypeVocabulary, self).__init__(terms)
104
105
89class IWebhook(Interface):106class IWebhook(Interface):
90107
91 export_as_webservice_entry(as_of='beta')108 export_as_webservice_entry(as_of='beta')
@@ -97,9 +114,7 @@
97 required=True, readonly=True,114 required=True, readonly=True,
98 description=_("The object for which this webhook receives events.")))115 description=_("The object for which this webhook receives events.")))
99 event_types = exported(List(116 event_types = exported(List(
100 TextLine(), title=_("Event types"),117 Choice(vocabulary='WebhookEventType'), title=_("Event types"),
101 description=_(
102 "The event types for which this webhook receives events."),
103 required=True, readonly=False))118 required=True, readonly=False))
104 registrant = exported(Reference(119 registrant = exported(Reference(
105 title=_("Registrant"), schema=IPerson, required=True, readonly=True,120 title=_("Registrant"), schema=IPerson, required=True, readonly=True,
106121
=== modified file 'lib/lp/services/webhooks/model.py'
--- lib/lp/services/webhooks/model.py 2015-08-04 13:59:25 +0000
+++ lib/lp/services/webhooks/model.py 2015-08-10 06:17:10 +0000
@@ -140,6 +140,8 @@
140 @event_types.setter140 @event_types.setter
141 def event_types(self, event_types):141 def event_types(self, event_types):
142 updated_data = self.json_data or {}142 updated_data = self.json_data or {}
143 # The correctness of the values is also checked by zope.schema,
144 # but best to be safe.
143 assert isinstance(event_types, (list, tuple))145 assert isinstance(event_types, (list, tuple))
144 assert all(isinstance(v, basestring) for v in event_types)146 assert all(isinstance(v, basestring) for v in event_types)
145 updated_data['event_types'] = event_types147 updated_data['event_types'] = event_types
146148
=== modified file 'lib/lp/services/webhooks/tests/test_browser.py'
--- lib/lp/services/webhooks/tests/test_browser.py 2015-08-04 13:18:24 +0000
+++ lib/lp/services/webhooks/tests/test_browser.py 2015-08-10 06:17:10 +0000
@@ -149,7 +149,8 @@
149 "+new-webhook", method="POST",149 "+new-webhook", method="POST",
150 form={150 form={
151 "field.delivery_url": "http://example.com/test",151 "field.delivery_url": "http://example.com/test",
152 "field.active": "on", "field.event_types.count": "0",152 "field.active": "on", "field.event_types-empty-marker": "1",
153 "field.event_types": "git:push:0.1",
153 "field.actions.new": "Add webhook"})154 "field.actions.new": "Add webhook"})
154 self.assertEqual([], view.errors)155 self.assertEqual([], view.errors)
155 hook = self.target.webhooks.one()156 hook = self.target.webhooks.one()
@@ -160,7 +161,7 @@
160 registrant=self.owner,161 registrant=self.owner,
161 delivery_url="http://example.com/test",162 delivery_url="http://example.com/test",
162 active=True,163 active=True,
163 event_types=[]))164 event_types=["git:push:0.1"]))
164165
165 def test_rejects_bad_scheme(self):166 def test_rejects_bad_scheme(self):
166 transaction.commit()167 transaction.commit()
@@ -168,7 +169,7 @@
168 "+new-webhook", method="POST",169 "+new-webhook", method="POST",
169 form={170 form={
170 "field.delivery_url": "ftp://example.com/test",171 "field.delivery_url": "ftp://example.com/test",
171 "field.active": "on", "field.event_types.count": "0",172 "field.active": "on", "field.event_types-empty-marker": "1",
172 "field.actions.new": "Add webhook"})173 "field.actions.new": "Add webhook"})
173 self.assertEqual(174 self.assertEqual(
174 ['delivery_url'], [error.field_name for error in view.errors])175 ['delivery_url'], [error.field_name for error in view.errors])
@@ -220,7 +221,7 @@
220 "+index", method="POST",221 "+index", method="POST",
221 form={222 form={
222 "field.delivery_url": "http://example.com/edited",223 "field.delivery_url": "http://example.com/edited",
223 "field.active": "off", "field.event_types.count": "0",224 "field.active": "off", "field.event_types-empty-marker": "1",
224 "field.actions.save": "Save webhook"})225 "field.actions.save": "Save webhook"})
225 self.assertEqual([], view.errors)226 self.assertEqual([], view.errors)
226 self.assertThat(227 self.assertThat(
@@ -236,7 +237,7 @@
236 "+index", method="POST",237 "+index", method="POST",
237 form={238 form={
238 "field.delivery_url": "ftp://example.com/edited",239 "field.delivery_url": "ftp://example.com/edited",
239 "field.active": "off", "field.event_types.count": "0",240 "field.active": "off", "field.event_types-empty-marker": "1",
240 "field.actions.save": "Save webhook"})241 "field.actions.save": "Save webhook"})
241 self.assertEqual(242 self.assertEqual(
242 ['delivery_url'], [error.field_name for error in view.errors])243 ['delivery_url'], [error.field_name for error in view.errors])
243244
=== modified file 'lib/lp/services/webhooks/tests/test_model.py'
--- lib/lp/services/webhooks/tests/test_model.py 2015-08-04 05:46:54 +0000
+++ lib/lp/services/webhooks/tests/test_model.py 2015-08-10 06:17:10 +0000
@@ -22,6 +22,7 @@
22from lp.testing import (22from lp.testing import (
23 admin_logged_in,23 admin_logged_in,
24 anonymous_logged_in,24 anonymous_logged_in,
25 ExpectedException,
25 login_person,26 login_person,
26 person_logged_in,27 person_logged_in,
27 StormStatementRecorder,28 StormStatementRecorder,
@@ -49,6 +50,24 @@
49 webhook.date_last_modified,50 webhook.date_last_modified,
50 GreaterThan(old_mtime))51 GreaterThan(old_mtime))
5152
53 def test_event_types(self):
54 # Webhook.event_types is a list of event type strings.
55 webhook = self.factory.makeWebhook()
56 with admin_logged_in():
57 self.assertContentEqual([], webhook.event_types)
58 webhook.event_types = ['foo', 'bar']
59 self.assertContentEqual(['foo', 'bar'], webhook.event_types)
60
61 def test_event_types_bad_structure(self):
62 # It's not possible to set Webhook.event_types to a list of the
63 # wrong type.
64 webhook = self.factory.makeWebhook()
65 with admin_logged_in():
66 self.assertContentEqual([], webhook.event_types)
67 with ExpectedException(AssertionError, '.*'):
68 webhook.event_types = ['foo', [1]]
69 self.assertContentEqual([], webhook.event_types)
70
5271
53class TestWebhookPermissions(TestCaseWithFactory):72class TestWebhookPermissions(TestCaseWithFactory):
5473
5574
=== modified file 'lib/lp/services/webhooks/tests/test_webservice.py'
--- lib/lp/services/webhooks/tests/test_webservice.py 2015-08-04 05:44:42 +0000
+++ lib/lp/services/webhooks/tests/test_webservice.py 2015-08-10 06:17:10 +0000
@@ -16,6 +16,7 @@
16 Is,16 Is,
17 KeysEqual,17 KeysEqual,
18 MatchesAll,18 MatchesAll,
19 MatchesStructure,
19 Not,20 Not,
20 )21 )
21from zope.security.proxy import removeSecurityProxy22from zope.security.proxy import removeSecurityProxy
@@ -73,7 +74,7 @@
73 old_mtime = representation['date_last_modified']74 old_mtime = representation['date_last_modified']
74 patch = json.dumps(75 patch = json.dumps(
75 {'active': False, 'delivery_url': 'http://example.com/ep2',76 {'active': False, 'delivery_url': 'http://example.com/ep2',
76 'event_types': ['foo', 'bar']})77 'event_types': ['git:push:0.1']})
77 self.webservice.patch(78 self.webservice.patch(
78 self.webhook_url, 'application/json', patch, api_version='devel')79 self.webhook_url, 'application/json', patch, api_version='devel')
79 representation = self.webservice.get(80 representation = self.webservice.get(
@@ -84,7 +85,33 @@
84 {'active': Equals(False),85 {'active': Equals(False),
85 'delivery_url': Equals('http://example.com/ep2'),86 'delivery_url': Equals('http://example.com/ep2'),
86 'date_last_modified': GreaterThan(old_mtime),87 'date_last_modified': GreaterThan(old_mtime),
87 'event_types': Equals(['foo', 'bar'])}))88 'event_types': Equals(['git:push:0.1'])}))
89
90 def test_patch_event_types(self):
91 representation = self.webservice.get(
92 self.webhook_url, api_version='devel').jsonBody()
93 self.assertThat(
94 representation, ContainsDict({'event_types': Equals([])}))
95
96 # Including a valid type in event_types works.
97 response = self.webservice.patch(
98 self.webhook_url, 'application/json',
99 json.dumps({'event_types': ['git:push:0.1']}), api_version='devel')
100 self.assertEqual(209, response.status)
101 representation = self.webservice.get(
102 self.webhook_url, api_version='devel').jsonBody()
103 self.assertThat(
104 representation,
105 ContainsDict({'event_types': Equals(['git:push:0.1'])}))
106
107 # But an unknown type is rejected.
108 response = self.webservice.patch(
109 self.webhook_url, 'application/json',
110 json.dumps({'event_types': ['hg:push:0.1']}), api_version='devel')
111 self.assertThat(response,
112 MatchesStructure.byEquality(
113 status=400,
114 body="event_types: u'hg:push:0.1' isn't a valid token"))
88115
89 def test_anon_forbidden(self):116 def test_anon_forbidden(self):
90 response = LaunchpadWebServiceCaller().get(117 response = LaunchpadWebServiceCaller().get(
@@ -279,14 +306,14 @@
279 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))306 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
280 response = self.webservice.named_post(307 response = self.webservice.named_post(
281 self.target_url, 'newWebhook',308 self.target_url, 'newWebhook',
282 delivery_url='http://example.com/ep', event_types=['foo', 'bar'],309 delivery_url='http://example.com/ep',
283 api_version='devel')310 event_types=['git:push:0.1'], api_version='devel')
284 self.assertEqual(201, response.status)311 self.assertEqual(201, response.status)
285312
286 representation = self.webservice.get(313 representation = self.webservice.get(
287 self.target_url + '/webhooks', api_version='devel').jsonBody()314 self.target_url + '/webhooks', api_version='devel').jsonBody()
288 self.assertContentEqual(315 self.assertContentEqual(
289 [('http://example.com/ep', ['foo', 'bar'], True)],316 [('http://example.com/ep', ['git:push:0.1'], True)],
290 [(entry['delivery_url'], entry['event_types'], entry['active'])317 [(entry['delivery_url'], entry['event_types'], entry['active'])
291 for entry in representation['entries']])318 for entry in representation['entries']])
292319
@@ -294,7 +321,7 @@
294 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))321 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
295 response = self.webservice.named_post(322 response = self.webservice.named_post(
296 self.target_url, 'newWebhook',323 self.target_url, 'newWebhook',
297 delivery_url='http://example.com/ep', event_types=['foo', 'bar'],324 delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
298 secret='sekrit', api_version='devel')325 secret='sekrit', api_version='devel')
299 self.assertEqual(201, response.status)326 self.assertEqual(201, response.status)
300327
@@ -310,7 +337,7 @@
310 webservice = LaunchpadWebServiceCaller()337 webservice = LaunchpadWebServiceCaller()
311 response = webservice.named_post(338 response = webservice.named_post(
312 self.target_url, 'newWebhook',339 self.target_url, 'newWebhook',
313 delivery_url='http://example.com/ep', event_types=['foo', 'bar'],340 delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
314 api_version='devel')341 api_version='devel')
315 self.assertEqual(401, response.status)342 self.assertEqual(401, response.status)
316 self.assertIn('launchpad.Edit', response.body)343 self.assertIn('launchpad.Edit', response.body)
@@ -318,7 +345,7 @@
318 def test_newWebhook_feature_flag_guard(self):345 def test_newWebhook_feature_flag_guard(self):
319 response = self.webservice.named_post(346 response = self.webservice.named_post(
320 self.target_url, 'newWebhook',347 self.target_url, 'newWebhook',
321 delivery_url='http://example.com/ep', event_types=['foo', 'bar'],348 delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
322 api_version='devel')349 api_version='devel')
323 self.assertEqual(401, response.status)350 self.assertEqual(401, response.status)
324 self.assertEqual(351 self.assertEqual(