Merge lp:~cjwatson/launchpad/snap-webhooks into lp:launchpad
- snap-webhooks
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17929 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-webhooks | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
810 lines (+269/-62) 16 files modified
database/schema/security.cfg (+5/-1) lib/lp/services/webhooks/interfaces.py (+2/-1) lib/lp/services/webhooks/model.py (+12/-1) lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt (+2/-2) lib/lp/services/webhooks/tests/test_browser.py (+55/-5) lib/lp/services/webhooks/tests/test_model.py (+57/-39) lib/lp/services/webhooks/tests/test_webservice.py (+12/-1) lib/lp/snappy/browser/snap.py (+10/-3) lib/lp/snappy/configure.zcml (+5/-1) lib/lp/snappy/interfaces/snap.py (+7/-3) lib/lp/snappy/interfaces/snapbuild.py (+7/-1) lib/lp/snappy/model/snap.py (+9/-2) lib/lp/snappy/model/snapbuild.py (+19/-1) lib/lp/snappy/subscribers/snapbuild.py (+30/-0) lib/lp/snappy/tests/test_snap.py (+12/-1) lib/lp/snappy/tests/test_snapbuild.py (+25/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-webhooks | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Celso Providelo (community) | Approve | ||
Launchpad code reviewers | Pending | ||
Review via email: mp+283193@code.launchpad.net |
Commit message
Add webhook support for snap packages.
Description of the change
Add webhook support for snap packages.
This currently only supports notifying about build status changes, using a webhook called "snap:build:0.1". I thought about notifying only terminal statuses, but on reflection a third-party service making snap build requests might well want to show an indication of a build being in progress, so this just notifies everything and the far end can sort out what it wants.
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
DB security changes are applied automatically, indeed.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'database/schema/security.cfg' | |||
2 | --- database/schema/security.cfg 2015-11-21 11:11:50 +0000 | |||
3 | +++ database/schema/security.cfg 2016-02-19 14:20:42 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | # | 3 | # |
9 | 4 | # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE | 4 | # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE |
10 | @@ -1003,6 +1003,8 @@ | |||
11 | 1003 | public.teamparticipation = SELECT | 1003 | public.teamparticipation = SELECT |
12 | 1004 | public.translationimportqueueentry = SELECT, INSERT, UPDATE | 1004 | public.translationimportqueueentry = SELECT, INSERT, UPDATE |
13 | 1005 | public.translationtemplatesbuild = SELECT, INSERT, UPDATE | 1005 | public.translationtemplatesbuild = SELECT, INSERT, UPDATE |
14 | 1006 | public.webhook = SELECT | ||
15 | 1007 | public.webhookjob = SELECT, INSERT | ||
16 | 1006 | type=user | 1008 | type=user |
17 | 1007 | 1009 | ||
18 | 1008 | [buildd_manager] | 1010 | [buildd_manager] |
19 | @@ -1429,6 +1431,8 @@ | |||
20 | 1429 | public.teamparticipation = SELECT, INSERT | 1431 | public.teamparticipation = SELECT, INSERT |
21 | 1430 | public.validpersoncache = SELECT | 1432 | public.validpersoncache = SELECT |
22 | 1431 | public.validpersonorteamcache = SELECT | 1433 | public.validpersonorteamcache = SELECT |
23 | 1434 | public.webhook = SELECT | ||
24 | 1435 | public.webhookjob = SELECT, INSERT | ||
25 | 1432 | public.wikiname = SELECT, INSERT | 1436 | public.wikiname = SELECT, INSERT |
26 | 1433 | public.xref = SELECT, INSERT | 1437 | public.xref = SELECT, INSERT |
27 | 1434 | type=group | 1438 | type=group |
28 | 1435 | 1439 | ||
29 | === modified file 'lib/lp/services/webhooks/interfaces.py' | |||
30 | --- lib/lp/services/webhooks/interfaces.py 2015-10-14 16:03:01 +0000 | |||
31 | +++ lib/lp/services/webhooks/interfaces.py 2016-02-19 14:20:42 +0000 | |||
32 | @@ -1,4 +1,4 @@ | |||
34 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
35 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
36 | 3 | 3 | ||
37 | 4 | """Webhook interfaces.""" | 4 | """Webhook interfaces.""" |
38 | @@ -76,6 +76,7 @@ | |||
39 | 76 | "bzr:push:0.1": "Bazaar push", | 76 | "bzr:push:0.1": "Bazaar push", |
40 | 77 | "git:push:0.1": "Git push", | 77 | "git:push:0.1": "Git push", |
41 | 78 | "merge-proposal:0.1": "Merge proposal", | 78 | "merge-proposal:0.1": "Merge proposal", |
42 | 79 | "snap:build:0.1": "Snap build", | ||
43 | 79 | } | 80 | } |
44 | 80 | 81 | ||
45 | 81 | 82 | ||
46 | 82 | 83 | ||
47 | === modified file 'lib/lp/services/webhooks/model.py' | |||
48 | --- lib/lp/services/webhooks/model.py 2015-11-11 18:12:24 +0000 | |||
49 | +++ lib/lp/services/webhooks/model.py 2016-02-19 14:20:42 +0000 | |||
50 | @@ -1,4 +1,4 @@ | |||
52 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
53 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
54 | 3 | 3 | ||
55 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
56 | @@ -102,6 +102,9 @@ | |||
57 | 102 | branch_id = Int(name='branch') | 102 | branch_id = Int(name='branch') |
58 | 103 | branch = Reference(branch_id, 'Branch.id') | 103 | branch = Reference(branch_id, 'Branch.id') |
59 | 104 | 104 | ||
60 | 105 | snap_id = Int(name='snap') | ||
61 | 106 | snap = Reference(snap_id, 'Snap.id') | ||
62 | 107 | |||
63 | 105 | registrant_id = Int(name='registrant', allow_none=False) | 108 | registrant_id = Int(name='registrant', allow_none=False) |
64 | 106 | registrant = Reference(registrant_id, 'Person.id') | 109 | registrant = Reference(registrant_id, 'Person.id') |
65 | 107 | date_created = DateTime(tzinfo=utc, allow_none=False) | 110 | date_created = DateTime(tzinfo=utc, allow_none=False) |
66 | @@ -119,6 +122,8 @@ | |||
67 | 119 | return self.git_repository | 122 | return self.git_repository |
68 | 120 | elif self.branch is not None: | 123 | elif self.branch is not None: |
69 | 121 | return self.branch | 124 | return self.branch |
70 | 125 | elif self.snap is not None: | ||
71 | 126 | return self.snap | ||
72 | 122 | else: | 127 | else: |
73 | 123 | raise AssertionError("No target.") | 128 | raise AssertionError("No target.") |
74 | 124 | 129 | ||
75 | @@ -172,11 +177,14 @@ | |||
76 | 172 | secret): | 177 | secret): |
77 | 173 | from lp.code.interfaces.branch import IBranch | 178 | from lp.code.interfaces.branch import IBranch |
78 | 174 | from lp.code.interfaces.gitrepository import IGitRepository | 179 | from lp.code.interfaces.gitrepository import IGitRepository |
79 | 180 | from lp.snappy.interfaces.snap import ISnap | ||
80 | 175 | hook = Webhook() | 181 | hook = Webhook() |
81 | 176 | if IGitRepository.providedBy(target): | 182 | if IGitRepository.providedBy(target): |
82 | 177 | hook.git_repository = target | 183 | hook.git_repository = target |
83 | 178 | elif IBranch.providedBy(target): | 184 | elif IBranch.providedBy(target): |
84 | 179 | hook.branch = target | 185 | hook.branch = target |
85 | 186 | elif ISnap.providedBy(target): | ||
86 | 187 | hook.snap = target | ||
87 | 180 | else: | 188 | else: |
88 | 181 | raise AssertionError("Unsupported target: %r" % (target,)) | 189 | raise AssertionError("Unsupported target: %r" % (target,)) |
89 | 182 | hook.registrant = registrant | 190 | hook.registrant = registrant |
90 | @@ -200,10 +208,13 @@ | |||
91 | 200 | def findByTarget(self, target): | 208 | def findByTarget(self, target): |
92 | 201 | from lp.code.interfaces.branch import IBranch | 209 | from lp.code.interfaces.branch import IBranch |
93 | 202 | from lp.code.interfaces.gitrepository import IGitRepository | 210 | from lp.code.interfaces.gitrepository import IGitRepository |
94 | 211 | from lp.snappy.interfaces.snap import ISnap | ||
95 | 203 | if IGitRepository.providedBy(target): | 212 | if IGitRepository.providedBy(target): |
96 | 204 | target_filter = Webhook.git_repository == target | 213 | target_filter = Webhook.git_repository == target |
97 | 205 | elif IBranch.providedBy(target): | 214 | elif IBranch.providedBy(target): |
98 | 206 | target_filter = Webhook.branch == target | 215 | target_filter = Webhook.branch == target |
99 | 216 | elif ISnap.providedBy(target): | ||
100 | 217 | target_filter = Webhook.snap == target | ||
101 | 207 | else: | 218 | else: |
102 | 208 | raise AssertionError("Unsupported target: %r" % (target,)) | 219 | raise AssertionError("Unsupported target: %r" % (target,)) |
103 | 209 | return IStore(Webhook).find(Webhook, target_filter).order_by( | 220 | return IStore(Webhook).find(Webhook, target_filter).order_by( |
104 | 210 | 221 | ||
105 | === modified file 'lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt' | |||
106 | --- lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-09-24 13:44:02 +0000 | |||
107 | +++ lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2016-02-19 14:20:42 +0000 | |||
108 | @@ -18,8 +18,8 @@ | |||
109 | 18 | <div> | 18 | <div> |
110 | 19 | <div class="beta" style="display: inline"> | 19 | <div class="beta" style="display: inline"> |
111 | 20 | <img class="beta" alt="[BETA]" src="/@@/beta" /></div> | 20 | <img class="beta" alt="[BETA]" src="/@@/beta" /></div> |
114 | 21 | The only currently supported events are Git and Bazaar pushes. We'll | 21 | The only currently supported events are Git and Bazaar pushes and |
115 | 22 | be rolling out webhooks for more soon. | 22 | snap package builds. We'll be rolling out webhooks for more soon. |
116 | 23 | </div> | 23 | </div> |
117 | 24 | <ul class="horizontal"> | 24 | <ul class="horizontal"> |
118 | 25 | <li> | 25 | <li> |
119 | 26 | 26 | ||
120 | === modified file 'lib/lp/services/webhooks/tests/test_browser.py' | |||
121 | --- lib/lp/services/webhooks/tests/test_browser.py 2015-10-26 11:47:38 +0000 | |||
122 | +++ lib/lp/services/webhooks/tests/test_browser.py 2016-02-19 14:20:42 +0000 | |||
123 | @@ -1,4 +1,4 @@ | |||
125 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
126 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
127 | 3 | 3 | ||
128 | 4 | """Unit tests for Webhook views.""" | 4 | """Unit tests for Webhook views.""" |
129 | @@ -17,6 +17,7 @@ | |||
130 | 17 | 17 | ||
131 | 18 | from lp.services.features.testing import FeatureFixture | 18 | from lp.services.features.testing import FeatureFixture |
132 | 19 | from lp.services.webapp.publisher import canonical_url | 19 | from lp.services.webapp.publisher import canonical_url |
133 | 20 | from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG | ||
134 | 20 | from lp.testing import ( | 21 | from lp.testing import ( |
135 | 21 | login_person, | 22 | login_person, |
136 | 22 | record_two_runs, | 23 | record_two_runs, |
137 | @@ -59,6 +60,9 @@ | |||
138 | 59 | def makeTarget(self): | 60 | def makeTarget(self): |
139 | 60 | return self.factory.makeGitRepository() | 61 | return self.factory.makeGitRepository() |
140 | 61 | 62 | ||
141 | 63 | def getTraversalStack(self, obj): | ||
142 | 64 | return [obj.target, obj] | ||
143 | 65 | |||
144 | 62 | 66 | ||
145 | 63 | class BranchTestHelpers: | 67 | class BranchTestHelpers: |
146 | 64 | 68 | ||
147 | @@ -71,6 +75,28 @@ | |||
148 | 71 | def makeTarget(self): | 75 | def makeTarget(self): |
149 | 72 | return self.factory.makeBranch() | 76 | return self.factory.makeBranch() |
150 | 73 | 77 | ||
151 | 78 | def getTraversalStack(self, obj): | ||
152 | 79 | return [obj.target, obj] | ||
153 | 80 | |||
154 | 81 | |||
155 | 82 | class SnapTestHelpers: | ||
156 | 83 | |||
157 | 84 | event_type = "snap:build:0.1" | ||
158 | 85 | expected_event_types = [ | ||
159 | 86 | ("snap:build:0.1", "Snap build"), | ||
160 | 87 | ] | ||
161 | 88 | |||
162 | 89 | def makeTarget(self): | ||
163 | 90 | self.useFixture(FeatureFixture({ | ||
164 | 91 | SNAP_FEATURE_FLAG: 'true', | ||
165 | 92 | 'webhooks.new.enabled': 'true', | ||
166 | 93 | })) | ||
167 | 94 | owner = self.factory.makePerson() | ||
168 | 95 | return self.factory.makeSnap(registrant=owner, owner=owner) | ||
169 | 96 | |||
170 | 97 | def getTraversalStack(self, obj): | ||
171 | 98 | return [obj] | ||
172 | 99 | |||
173 | 74 | 100 | ||
174 | 75 | class WebhookTargetViewTestHelpers: | 101 | class WebhookTargetViewTestHelpers: |
175 | 76 | 102 | ||
176 | @@ -84,8 +110,8 @@ | |||
177 | 84 | def makeView(self, name, **kwargs): | 110 | def makeView(self, name, **kwargs): |
178 | 85 | view = create_view(self.target, name, principal=self.owner, **kwargs) | 111 | view = create_view(self.target, name, principal=self.owner, **kwargs) |
179 | 86 | # To test the breadcrumbs we need a correct traversal stack. | 112 | # To test the breadcrumbs we need a correct traversal stack. |
182 | 87 | view.request.traversed_objects = [ | 113 | view.request.traversed_objects = ( |
183 | 88 | self.target.target, self.target, view] | 114 | self.getTraversalStack(self.target) + [view]) |
184 | 89 | view.initialize() | 115 | view.initialize() |
185 | 90 | return view | 116 | return view |
186 | 91 | 117 | ||
187 | @@ -162,6 +188,12 @@ | |||
188 | 162 | pass | 188 | pass |
189 | 163 | 189 | ||
190 | 164 | 190 | ||
191 | 191 | class TestWebhooksViewSnap( | ||
192 | 192 | TestWebhooksViewBase, SnapTestHelpers, TestCaseWithFactory): | ||
193 | 193 | |||
194 | 194 | pass | ||
195 | 195 | |||
196 | 196 | |||
197 | 165 | class TestWebhookAddViewBase(WebhookTargetViewTestHelpers): | 197 | class TestWebhookAddViewBase(WebhookTargetViewTestHelpers): |
198 | 166 | 198 | ||
199 | 167 | layer = DatabaseFunctionalLayer | 199 | layer = DatabaseFunctionalLayer |
200 | @@ -254,6 +286,12 @@ | |||
201 | 254 | pass | 286 | pass |
202 | 255 | 287 | ||
203 | 256 | 288 | ||
204 | 289 | class TestWebhookAddViewSnap( | ||
205 | 290 | TestWebhookAddViewBase, SnapTestHelpers, TestCaseWithFactory): | ||
206 | 291 | |||
207 | 292 | pass | ||
208 | 293 | |||
209 | 294 | |||
210 | 257 | class WebhookViewTestHelpers: | 295 | class WebhookViewTestHelpers: |
211 | 258 | 296 | ||
212 | 259 | def setUp(self): | 297 | def setUp(self): |
213 | @@ -268,8 +306,8 @@ | |||
214 | 268 | def makeView(self, name, **kwargs): | 306 | def makeView(self, name, **kwargs): |
215 | 269 | view = create_view(self.webhook, name, principal=self.owner, **kwargs) | 307 | view = create_view(self.webhook, name, principal=self.owner, **kwargs) |
216 | 270 | # To test the breadcrumbs we need a correct traversal stack. | 308 | # To test the breadcrumbs we need a correct traversal stack. |
219 | 271 | view.request.traversed_objects = [ | 309 | view.request.traversed_objects = ( |
220 | 272 | self.target.target, self.target, self.webhook, view] | 310 | self.getTraversalStack(self.target) + [self.webhook, view]) |
221 | 273 | view.initialize() | 311 | view.initialize() |
222 | 274 | return view | 312 | return view |
223 | 275 | 313 | ||
224 | @@ -350,6 +388,12 @@ | |||
225 | 350 | pass | 388 | pass |
226 | 351 | 389 | ||
227 | 352 | 390 | ||
228 | 391 | class TestWebhookViewSnap( | ||
229 | 392 | TestWebhookViewBase, SnapTestHelpers, TestCaseWithFactory): | ||
230 | 393 | |||
231 | 394 | pass | ||
232 | 395 | |||
233 | 396 | |||
234 | 353 | class TestWebhookDeleteViewBase(WebhookViewTestHelpers): | 397 | class TestWebhookDeleteViewBase(WebhookViewTestHelpers): |
235 | 354 | 398 | ||
236 | 355 | layer = DatabaseFunctionalLayer | 399 | layer = DatabaseFunctionalLayer |
237 | @@ -394,3 +438,9 @@ | |||
238 | 394 | TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory): | 438 | TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory): |
239 | 395 | 439 | ||
240 | 396 | pass | 440 | pass |
241 | 441 | |||
242 | 442 | |||
243 | 443 | class TestWebhookDeleteViewSnap( | ||
244 | 444 | TestWebhookDeleteViewBase, SnapTestHelpers, TestCaseWithFactory): | ||
245 | 445 | |||
246 | 446 | pass | ||
247 | 397 | 447 | ||
248 | === modified file 'lib/lp/services/webhooks/tests/test_model.py' | |||
249 | --- lib/lp/services/webhooks/tests/test_model.py 2015-10-14 16:03:01 +0000 | |||
250 | +++ lib/lp/services/webhooks/tests/test_model.py 2016-02-19 14:20:42 +0000 | |||
251 | @@ -1,4 +1,4 @@ | |||
253 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
254 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
255 | 3 | 3 | ||
256 | 4 | from lazr.lifecycle.event import ObjectModifiedEvent | 4 | from lazr.lifecycle.event import ObjectModifiedEvent |
257 | @@ -17,6 +17,7 @@ | |||
258 | 17 | from lp.app.enums import InformationType | 17 | from lp.app.enums import InformationType |
259 | 18 | from lp.registry.enums import BranchSharingPolicy | 18 | from lp.registry.enums import BranchSharingPolicy |
260 | 19 | from lp.services.database.interfaces import IStore | 19 | from lp.services.database.interfaces import IStore |
261 | 20 | from lp.services.features.testing import FeatureFixture | ||
262 | 20 | from lp.services.webapp.authorization import check_permission | 21 | from lp.services.webapp.authorization import check_permission |
263 | 21 | from lp.services.webhooks.interfaces import ( | 22 | from lp.services.webhooks.interfaces import ( |
264 | 22 | IWebhook, | 23 | IWebhook, |
265 | @@ -26,6 +27,7 @@ | |||
266 | 26 | WebhookJob, | 27 | WebhookJob, |
267 | 27 | WebhookSet, | 28 | WebhookSet, |
268 | 28 | ) | 29 | ) |
269 | 30 | from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG | ||
270 | 29 | from lp.testing import ( | 31 | from lp.testing import ( |
271 | 30 | admin_logged_in, | 32 | admin_logged_in, |
272 | 31 | anonymous_logged_in, | 33 | anonymous_logged_in, |
273 | @@ -204,6 +206,45 @@ | |||
274 | 204 | login_person(target.owner) | 206 | login_person(target.owner) |
275 | 205 | self.assertTrue(WebhookSet._checkVisibility(target, target.owner)) | 207 | self.assertTrue(WebhookSet._checkVisibility(target, target.owner)) |
276 | 206 | 208 | ||
277 | 209 | def test_trigger(self): | ||
278 | 210 | owner = self.factory.makePerson() | ||
279 | 211 | target1 = self.makeTarget(owner=owner) | ||
280 | 212 | target2 = self.makeTarget(owner=owner) | ||
281 | 213 | hook1a = self.factory.makeWebhook( | ||
282 | 214 | target=target1, event_types=[]) | ||
283 | 215 | hook1b = self.factory.makeWebhook( | ||
284 | 216 | target=target1, event_types=[self.event_type]) | ||
285 | 217 | hook2a = self.factory.makeWebhook( | ||
286 | 218 | target=target2, event_types=[self.event_type]) | ||
287 | 219 | hook2b = self.factory.makeWebhook( | ||
288 | 220 | target=target2, event_types=[self.event_type], active=False) | ||
289 | 221 | |||
290 | 222 | # Only webhooks subscribed to the relevant target and event type | ||
291 | 223 | # are triggered. | ||
292 | 224 | getUtility(IWebhookSet).trigger( | ||
293 | 225 | target1, self.event_type, {'some': 'payload'}) | ||
294 | 226 | with admin_logged_in(): | ||
295 | 227 | self.assertThat(list(hook1a.deliveries), HasLength(0)) | ||
296 | 228 | self.assertThat(list(hook1b.deliveries), HasLength(1)) | ||
297 | 229 | self.assertThat(list(hook2a.deliveries), HasLength(0)) | ||
298 | 230 | self.assertThat(list(hook2b.deliveries), HasLength(0)) | ||
299 | 231 | delivery = hook1b.deliveries.one() | ||
300 | 232 | self.assertEqual(delivery.payload, {'some': 'payload'}) | ||
301 | 233 | |||
302 | 234 | # Disabled webhooks aren't triggered. | ||
303 | 235 | getUtility(IWebhookSet).trigger( | ||
304 | 236 | target2, self.event_type, {'other': 'payload'}) | ||
305 | 237 | with admin_logged_in(): | ||
306 | 238 | self.assertThat(list(hook1a.deliveries), HasLength(0)) | ||
307 | 239 | self.assertThat(list(hook1b.deliveries), HasLength(1)) | ||
308 | 240 | self.assertThat(list(hook2a.deliveries), HasLength(1)) | ||
309 | 241 | self.assertThat(list(hook2b.deliveries), HasLength(0)) | ||
310 | 242 | delivery = hook2a.deliveries.one() | ||
311 | 243 | self.assertEqual(delivery.payload, {'other': 'payload'}) | ||
312 | 244 | |||
313 | 245 | |||
314 | 246 | class TestWebhookSetMergeProposalBase(TestWebhookSetBase): | ||
315 | 247 | |||
316 | 207 | def test__checkVisibility_private_artifact(self): | 248 | def test__checkVisibility_private_artifact(self): |
317 | 208 | owner = self.factory.makePerson() | 249 | owner = self.factory.makePerson() |
318 | 209 | target = self.makeTarget( | 250 | target = self.makeTarget( |
319 | @@ -248,42 +289,6 @@ | |||
320 | 248 | self.assertFalse( | 289 | self.assertFalse( |
321 | 249 | WebhookSet._checkVisibility(mp2, mp2.merge_target.owner)) | 290 | WebhookSet._checkVisibility(mp2, mp2.merge_target.owner)) |
322 | 250 | 291 | ||
323 | 251 | def test_trigger(self): | ||
324 | 252 | owner = self.factory.makePerson() | ||
325 | 253 | target1 = self.makeTarget(owner=owner) | ||
326 | 254 | target2 = self.makeTarget(owner=owner) | ||
327 | 255 | hook1a = self.factory.makeWebhook( | ||
328 | 256 | target=target1, event_types=[]) | ||
329 | 257 | hook1b = self.factory.makeWebhook( | ||
330 | 258 | target=target1, event_types=[self.event_type]) | ||
331 | 259 | hook2a = self.factory.makeWebhook( | ||
332 | 260 | target=target2, event_types=[self.event_type]) | ||
333 | 261 | hook2b = self.factory.makeWebhook( | ||
334 | 262 | target=target2, event_types=[self.event_type], active=False) | ||
335 | 263 | |||
336 | 264 | # Only webhooks subscribed to the relevant target and event type | ||
337 | 265 | # are triggered. | ||
338 | 266 | getUtility(IWebhookSet).trigger( | ||
339 | 267 | target1, self.event_type, {'some': 'payload'}) | ||
340 | 268 | with admin_logged_in(): | ||
341 | 269 | self.assertThat(list(hook1a.deliveries), HasLength(0)) | ||
342 | 270 | self.assertThat(list(hook1b.deliveries), HasLength(1)) | ||
343 | 271 | self.assertThat(list(hook2a.deliveries), HasLength(0)) | ||
344 | 272 | self.assertThat(list(hook2b.deliveries), HasLength(0)) | ||
345 | 273 | delivery = hook1b.deliveries.one() | ||
346 | 274 | self.assertEqual(delivery.payload, {'some': 'payload'}) | ||
347 | 275 | |||
348 | 276 | # Disabled webhooks aren't triggered. | ||
349 | 277 | getUtility(IWebhookSet).trigger( | ||
350 | 278 | target2, self.event_type, {'other': 'payload'}) | ||
351 | 279 | with admin_logged_in(): | ||
352 | 280 | self.assertThat(list(hook1a.deliveries), HasLength(0)) | ||
353 | 281 | self.assertThat(list(hook1b.deliveries), HasLength(1)) | ||
354 | 282 | self.assertThat(list(hook2a.deliveries), HasLength(1)) | ||
355 | 283 | self.assertThat(list(hook2b.deliveries), HasLength(0)) | ||
356 | 284 | delivery = hook2a.deliveries.one() | ||
357 | 285 | self.assertEqual(delivery.payload, {'other': 'payload'}) | ||
358 | 286 | |||
359 | 287 | def test_trigger_skips_invisible(self): | 292 | def test_trigger_skips_invisible(self): |
360 | 288 | # No webhooks are dispatched if the visibility check fails. | 293 | # No webhooks are dispatched if the visibility check fails. |
361 | 289 | project = self.factory.makeProduct( | 294 | project = self.factory.makeProduct( |
362 | @@ -344,7 +349,8 @@ | |||
363 | 344 | self.assertEqual(delivery.payload, {'some': 'payload'}) | 349 | self.assertEqual(delivery.payload, {'some': 'payload'}) |
364 | 345 | 350 | ||
365 | 346 | 351 | ||
367 | 347 | class TestWebhookSetGitRepository(TestWebhookSetBase, TestCaseWithFactory): | 352 | class TestWebhookSetGitRepository( |
368 | 353 | TestWebhookSetMergeProposalBase, TestCaseWithFactory): | ||
369 | 348 | 354 | ||
370 | 349 | event_type = 'git:push:0.1' | 355 | event_type = 'git:push:0.1' |
371 | 350 | 356 | ||
372 | @@ -366,7 +372,8 @@ | |||
373 | 366 | reviewer=reviewer) | 372 | reviewer=reviewer) |
374 | 367 | 373 | ||
375 | 368 | 374 | ||
377 | 369 | class TestWebhookSetBranch(TestWebhookSetBase, TestCaseWithFactory): | 375 | class TestWebhookSetBranch( |
378 | 376 | TestWebhookSetMergeProposalBase, TestCaseWithFactory): | ||
379 | 370 | 377 | ||
380 | 371 | event_type = 'bzr:push:0.1' | 378 | event_type = 'bzr:push:0.1' |
381 | 372 | 379 | ||
382 | @@ -384,3 +391,14 @@ | |||
383 | 384 | return self.factory.makeBranchMergeProposal( | 391 | return self.factory.makeBranchMergeProposal( |
384 | 385 | registrant=owner, target_branch=target, source_branch=source, | 392 | registrant=owner, target_branch=target, source_branch=source, |
385 | 386 | reviewer=reviewer) | 393 | reviewer=reviewer) |
386 | 394 | |||
387 | 395 | |||
388 | 396 | class TestWebhookSetSnap(TestWebhookSetBase, TestCaseWithFactory): | ||
389 | 397 | |||
390 | 398 | event_type = 'snap:build:0.1' | ||
391 | 399 | |||
392 | 400 | def makeTarget(self, owner=None, **kwargs): | ||
393 | 401 | self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: 'true'})) | ||
394 | 402 | if owner is None: | ||
395 | 403 | owner = self.factory.makePerson() | ||
396 | 404 | return self.factory.makeSnap(registrant=owner, owner=owner, **kwargs) | ||
397 | 387 | 405 | ||
398 | === modified file 'lib/lp/services/webhooks/tests/test_webservice.py' | |||
399 | --- lib/lp/services/webhooks/tests/test_webservice.py 2015-10-13 16:58:20 +0000 | |||
400 | +++ lib/lp/services/webhooks/tests/test_webservice.py 2016-02-19 14:20:42 +0000 | |||
401 | @@ -1,4 +1,4 @@ | |||
403 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
404 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
405 | 3 | 3 | ||
406 | 4 | """Tests for the webhook webservice objects.""" | 4 | """Tests for the webhook webservice objects.""" |
407 | @@ -23,6 +23,7 @@ | |||
408 | 23 | 23 | ||
409 | 24 | from lp.services.features.testing import FeatureFixture | 24 | from lp.services.features.testing import FeatureFixture |
410 | 25 | from lp.services.webapp.interfaces import OAuthPermission | 25 | from lp.services.webapp.interfaces import OAuthPermission |
411 | 26 | from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG | ||
412 | 26 | from lp.testing import ( | 27 | from lp.testing import ( |
413 | 27 | api_url, | 28 | api_url, |
414 | 28 | person_logged_in, | 29 | person_logged_in, |
415 | @@ -370,3 +371,13 @@ | |||
416 | 370 | 371 | ||
417 | 371 | def makeTarget(self): | 372 | def makeTarget(self): |
418 | 372 | return self.factory.makeBranch() | 373 | return self.factory.makeBranch() |
419 | 374 | |||
420 | 375 | |||
421 | 376 | class TestWebhookTargetSnap(TestWebhookTargetBase, TestCaseWithFactory): | ||
422 | 377 | |||
423 | 378 | event_type = 'snap:build:0.1' | ||
424 | 379 | |||
425 | 380 | def makeTarget(self): | ||
426 | 381 | self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: 'true'})) | ||
427 | 382 | owner = self.factory.makePerson() | ||
428 | 383 | return self.factory.makeSnap(registrant=owner, owner=owner) | ||
429 | 373 | 384 | ||
430 | === modified file 'lib/lp/snappy/browser/snap.py' | |||
431 | --- lib/lp/snappy/browser/snap.py 2016-02-05 06:05:50 +0000 | |||
432 | +++ lib/lp/snappy/browser/snap.py 2016-02-19 14:20:42 +0000 | |||
433 | @@ -1,4 +1,4 @@ | |||
435 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
436 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
437 | 3 | 3 | ||
438 | 4 | """Snap views.""" | 4 | """Snap views.""" |
439 | @@ -65,6 +65,7 @@ | |||
440 | 65 | Breadcrumb, | 65 | Breadcrumb, |
441 | 66 | NameBreadcrumb, | 66 | NameBreadcrumb, |
442 | 67 | ) | 67 | ) |
443 | 68 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin | ||
444 | 68 | from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget | 69 | from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget |
445 | 69 | from lp.snappy.interfaces.snap import ( | 70 | from lp.snappy.interfaces.snap import ( |
446 | 70 | ISnap, | 71 | ISnap, |
447 | @@ -82,7 +83,7 @@ | |||
448 | 82 | from lp.soyuz.interfaces.archive import IArchive | 83 | from lp.soyuz.interfaces.archive import IArchive |
449 | 83 | 84 | ||
450 | 84 | 85 | ||
452 | 85 | class SnapNavigation(Navigation): | 86 | class SnapNavigation(WebhookTargetNavigationMixin, Navigation): |
453 | 86 | usedfor = ISnap | 87 | usedfor = ISnap |
454 | 87 | 88 | ||
455 | 88 | @stepthrough('+build') | 89 | @stepthrough('+build') |
456 | @@ -110,7 +111,7 @@ | |||
457 | 110 | 111 | ||
458 | 111 | facet = 'overview' | 112 | facet = 'overview' |
459 | 112 | 113 | ||
461 | 113 | links = ('edit', 'delete', 'admin') | 114 | links = ('admin', 'edit', 'webhooks', 'delete') |
462 | 114 | 115 | ||
463 | 115 | @enabled_with_permission('launchpad.Admin') | 116 | @enabled_with_permission('launchpad.Admin') |
464 | 116 | def admin(self): | 117 | def admin(self): |
465 | @@ -121,6 +122,12 @@ | |||
466 | 121 | return Link('+edit', 'Edit snap package', icon='edit') | 122 | return Link('+edit', 'Edit snap package', icon='edit') |
467 | 122 | 123 | ||
468 | 123 | @enabled_with_permission('launchpad.Edit') | 124 | @enabled_with_permission('launchpad.Edit') |
469 | 125 | def webhooks(self): | ||
470 | 126 | return Link( | ||
471 | 127 | '+webhooks', 'Manage webhooks', icon='edit', | ||
472 | 128 | enabled=bool(getFeatureFlag('webhooks.new.enabled'))) | ||
473 | 129 | |||
474 | 130 | @enabled_with_permission('launchpad.Edit') | ||
475 | 124 | def delete(self): | 131 | def delete(self): |
476 | 125 | return Link('+delete', 'Delete snap package', icon='trash-icon') | 132 | return Link('+delete', 'Delete snap package', icon='trash-icon') |
477 | 126 | 133 | ||
478 | 127 | 134 | ||
479 | === modified file 'lib/lp/snappy/configure.zcml' | |||
480 | --- lib/lp/snappy/configure.zcml 2015-09-25 17:26:03 +0000 | |||
481 | +++ lib/lp/snappy/configure.zcml 2016-02-19 14:20:42 +0000 | |||
482 | @@ -1,4 +1,4 @@ | |||
484 | 1 | <!-- Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
485 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
486 | 3 | --> | 3 | --> |
487 | 4 | 4 | ||
488 | @@ -52,6 +52,10 @@ | |||
489 | 52 | permission="launchpad.Admin" | 52 | permission="launchpad.Admin" |
490 | 53 | interface="lp.snappy.interfaces.snapbuild.ISnapBuildAdmin" /> | 53 | interface="lp.snappy.interfaces.snapbuild.ISnapBuildAdmin" /> |
491 | 54 | </class> | 54 | </class> |
492 | 55 | <subscriber | ||
493 | 56 | for="lp.snappy.interfaces.snapbuild.ISnapBuild | ||
494 | 57 | lp.snappy.interfaces.snapbuild.ISnapBuildStatusChangedEvent" | ||
495 | 58 | handler="lp.snappy.subscribers.snapbuild.snap_build_status_changed" /> | ||
496 | 55 | 59 | ||
497 | 56 | <!-- SnapBuildSet --> | 60 | <!-- SnapBuildSet --> |
498 | 57 | <securedutility | 61 | <securedutility |
499 | 58 | 62 | ||
500 | === modified file 'lib/lp/snappy/interfaces/snap.py' | |||
501 | --- lib/lp/snappy/interfaces/snap.py 2016-02-05 06:05:50 +0000 | |||
502 | +++ lib/lp/snappy/interfaces/snap.py 2016-02-19 14:20:42 +0000 | |||
503 | @@ -1,4 +1,4 @@ | |||
505 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
506 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
507 | 3 | 3 | ||
508 | 4 | """Snap package interfaces.""" | 4 | """Snap package interfaces.""" |
509 | @@ -18,6 +18,7 @@ | |||
510 | 18 | 'SNAP_FEATURE_FLAG', | 18 | 'SNAP_FEATURE_FLAG', |
511 | 19 | 'SNAP_PRIVATE_FEATURE_FLAG', | 19 | 'SNAP_PRIVATE_FEATURE_FLAG', |
512 | 20 | 'SNAP_TESTING_FLAGS', | 20 | 'SNAP_TESTING_FLAGS', |
513 | 21 | 'SNAP_WEBHOOKS_FEATURE_FLAG', | ||
514 | 21 | 'SnapBuildAlreadyPending', | 22 | 'SnapBuildAlreadyPending', |
515 | 22 | 'SnapBuildArchiveOwnerMismatch', | 23 | 'SnapBuildArchiveOwnerMismatch', |
516 | 23 | 'SnapBuildDisallowedArchitecture', | 24 | 'SnapBuildDisallowedArchitecture', |
517 | @@ -85,18 +86,21 @@ | |||
518 | 85 | PersonChoice, | 86 | PersonChoice, |
519 | 86 | PublicPersonChoice, | 87 | PublicPersonChoice, |
520 | 87 | ) | 88 | ) |
521 | 89 | from lp.services.webhooks.interfaces import IWebhookTarget | ||
522 | 88 | from lp.soyuz.interfaces.archive import IArchive | 90 | from lp.soyuz.interfaces.archive import IArchive |
523 | 89 | from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries | 91 | from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries |
524 | 90 | 92 | ||
525 | 91 | 93 | ||
526 | 92 | SNAP_FEATURE_FLAG = u"snap.allow_new" | 94 | SNAP_FEATURE_FLAG = u"snap.allow_new" |
527 | 93 | SNAP_PRIVATE_FEATURE_FLAG = u"snap.allow_private" | 95 | SNAP_PRIVATE_FEATURE_FLAG = u"snap.allow_private" |
528 | 96 | SNAP_WEBHOOKS_FEATURE_FLAG = u"snap.webhooks.enabled" | ||
529 | 94 | 97 | ||
530 | 95 | 98 | ||
531 | 96 | SNAP_TESTING_FLAGS = { | 99 | SNAP_TESTING_FLAGS = { |
532 | 97 | SNAP_FEATURE_FLAG: u"on", | 100 | SNAP_FEATURE_FLAG: u"on", |
533 | 98 | SNAP_PRIVATE_FEATURE_FLAG: u"on", | 101 | SNAP_PRIVATE_FEATURE_FLAG: u"on", |
535 | 99 | } | 102 | SNAP_WEBHOOKS_FEATURE_FLAG: u"on", |
536 | 103 | } | ||
537 | 100 | 104 | ||
538 | 101 | 105 | ||
539 | 102 | @error_status(httplib.BAD_REQUEST) | 106 | @error_status(httplib.BAD_REQUEST) |
540 | @@ -293,7 +297,7 @@ | |||
541 | 293 | value_type=Reference(schema=Interface), readonly=True))) | 297 | value_type=Reference(schema=Interface), readonly=True))) |
542 | 294 | 298 | ||
543 | 295 | 299 | ||
545 | 296 | class ISnapEdit(Interface): | 300 | class ISnapEdit(IWebhookTarget): |
546 | 297 | """`ISnap` methods that require launchpad.Edit permission.""" | 301 | """`ISnap` methods that require launchpad.Edit permission.""" |
547 | 298 | 302 | ||
548 | 299 | @export_destructor_operation() | 303 | @export_destructor_operation() |
549 | 300 | 304 | ||
550 | === modified file 'lib/lp/snappy/interfaces/snapbuild.py' | |||
551 | --- lib/lp/snappy/interfaces/snapbuild.py 2015-07-23 16:41:12 +0000 | |||
552 | +++ lib/lp/snappy/interfaces/snapbuild.py 2016-02-19 14:20:42 +0000 | |||
553 | @@ -1,4 +1,4 @@ | |||
555 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
556 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
557 | 3 | 3 | ||
558 | 4 | """Snap package build interfaces.""" | 4 | """Snap package build interfaces.""" |
559 | @@ -8,6 +8,7 @@ | |||
560 | 8 | __all__ = [ | 8 | __all__ = [ |
561 | 9 | 'ISnapBuild', | 9 | 'ISnapBuild', |
562 | 10 | 'ISnapBuildSet', | 10 | 'ISnapBuildSet', |
563 | 11 | 'ISnapBuildStatusChangedEvent', | ||
564 | 11 | 'ISnapFile', | 12 | 'ISnapFile', |
565 | 12 | ] | 13 | ] |
566 | 13 | 14 | ||
567 | @@ -20,6 +21,7 @@ | |||
568 | 20 | operation_parameters, | 21 | operation_parameters, |
569 | 21 | ) | 22 | ) |
570 | 22 | from lazr.restful.fields import Reference | 23 | from lazr.restful.fields import Reference |
571 | 24 | from zope.component.interfaces import IObjectEvent | ||
572 | 23 | from zope.interface import Interface | 25 | from zope.interface import Interface |
573 | 24 | from zope.schema import ( | 26 | from zope.schema import ( |
574 | 25 | Bool, | 27 | Bool, |
575 | @@ -39,6 +41,10 @@ | |||
576 | 39 | from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries | 41 | from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries |
577 | 40 | 42 | ||
578 | 41 | 43 | ||
579 | 44 | class ISnapBuildStatusChangedEvent(IObjectEvent): | ||
580 | 45 | """The status of a snap package build changed.""" | ||
581 | 46 | |||
582 | 47 | |||
583 | 42 | class ISnapFile(Interface): | 48 | class ISnapFile(Interface): |
584 | 43 | """A file produced by a snap package build.""" | 49 | """A file produced by a snap package build.""" |
585 | 44 | 50 | ||
586 | 45 | 51 | ||
587 | === modified file 'lib/lp/snappy/model/snap.py' | |||
588 | --- lib/lp/snappy/model/snap.py 2016-02-05 06:05:50 +0000 | |||
589 | +++ lib/lp/snappy/model/snap.py 2016-02-19 14:20:42 +0000 | |||
590 | @@ -1,4 +1,4 @@ | |||
592 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
593 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
594 | 3 | 3 | ||
595 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
596 | @@ -71,6 +71,8 @@ | |||
597 | 71 | ) | 71 | ) |
598 | 72 | from lp.services.features import getFeatureFlag | 72 | from lp.services.features import getFeatureFlag |
599 | 73 | from lp.services.webapp.interfaces import ILaunchBag | 73 | from lp.services.webapp.interfaces import ILaunchBag |
600 | 74 | from lp.services.webhooks.interfaces import IWebhookSet | ||
601 | 75 | from lp.services.webhooks.model import WebhookTargetMixin | ||
602 | 74 | from lp.snappy.interfaces.snap import ( | 76 | from lp.snappy.interfaces.snap import ( |
603 | 75 | BadSnapSearchContext, | 77 | BadSnapSearchContext, |
604 | 76 | CannotDeleteSnap, | 78 | CannotDeleteSnap, |
605 | @@ -110,7 +112,7 @@ | |||
606 | 110 | 112 | ||
607 | 111 | 113 | ||
608 | 112 | @implementer(ISnap, IHasOwner) | 114 | @implementer(ISnap, IHasOwner) |
610 | 113 | class Snap(Storm): | 115 | class Snap(Storm, WebhookTargetMixin): |
611 | 114 | """See `ISnap`.""" | 116 | """See `ISnap`.""" |
612 | 115 | 117 | ||
613 | 116 | __storm_table__ = 'Snap' | 118 | __storm_table__ = 'Snap' |
614 | @@ -169,6 +171,10 @@ | |||
615 | 169 | self.private = private | 171 | self.private = private |
616 | 170 | 172 | ||
617 | 171 | @property | 173 | @property |
618 | 174 | def valid_webhook_event_types(self): | ||
619 | 175 | return ["snap:build:0.1"] | ||
620 | 176 | |||
621 | 177 | @property | ||
622 | 172 | def git_ref(self): | 178 | def git_ref(self): |
623 | 173 | """See `ISnap`.""" | 179 | """See `ISnap`.""" |
624 | 174 | if self.git_repository is not None: | 180 | if self.git_repository is not None: |
625 | @@ -353,6 +359,7 @@ | |||
626 | 353 | raise CannotDeleteSnap("Cannot delete a snap package with builds.") | 359 | raise CannotDeleteSnap("Cannot delete a snap package with builds.") |
627 | 354 | store = IStore(Snap) | 360 | store = IStore(Snap) |
628 | 355 | store.find(SnapArch, SnapArch.snap == self).remove() | 361 | store.find(SnapArch, SnapArch.snap == self).remove() |
629 | 362 | getUtility(IWebhookSet).delete(self.webhooks) | ||
630 | 356 | store.remove(self) | 363 | store.remove(self) |
631 | 357 | 364 | ||
632 | 358 | 365 | ||
633 | 359 | 366 | ||
634 | === modified file 'lib/lp/snappy/model/snapbuild.py' | |||
635 | --- lib/lp/snappy/model/snapbuild.py 2016-02-04 00:45:12 +0000 | |||
636 | +++ lib/lp/snappy/model/snapbuild.py 2016-02-19 14:20:42 +0000 | |||
637 | @@ -1,4 +1,4 @@ | |||
639 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
640 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
641 | 3 | 3 | ||
642 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
643 | @@ -22,6 +22,8 @@ | |||
644 | 22 | ) | 22 | ) |
645 | 23 | from storm.store import EmptyResultSet | 23 | from storm.store import EmptyResultSet |
646 | 24 | from zope.component import getUtility | 24 | from zope.component import getUtility |
647 | 25 | from zope.component.interfaces import ObjectEvent | ||
648 | 26 | from zope.event import notify | ||
649 | 25 | from zope.interface import implementer | 27 | from zope.interface import implementer |
650 | 26 | 28 | ||
651 | 27 | from lp.app.errors import NotFoundError | 29 | from lp.app.errors import NotFoundError |
652 | @@ -59,6 +61,7 @@ | |||
653 | 59 | from lp.snappy.interfaces.snapbuild import ( | 61 | from lp.snappy.interfaces.snapbuild import ( |
654 | 60 | ISnapBuild, | 62 | ISnapBuild, |
655 | 61 | ISnapBuildSet, | 63 | ISnapBuildSet, |
656 | 64 | ISnapBuildStatusChangedEvent, | ||
657 | 62 | ISnapFile, | 65 | ISnapFile, |
658 | 63 | ) | 66 | ) |
659 | 64 | from lp.snappy.mail.snapbuild import SnapBuildMailer | 67 | from lp.snappy.mail.snapbuild import SnapBuildMailer |
660 | @@ -67,6 +70,11 @@ | |||
661 | 67 | from lp.soyuz.model.distroarchseries import DistroArchSeries | 70 | from lp.soyuz.model.distroarchseries import DistroArchSeries |
662 | 68 | 71 | ||
663 | 69 | 72 | ||
664 | 73 | @implementer(ISnapBuildStatusChangedEvent) | ||
665 | 74 | class SnapBuildStatusChangedEvent(ObjectEvent): | ||
666 | 75 | """See `ISnapBuildStatusChangedEvent`.""" | ||
667 | 76 | |||
668 | 77 | |||
669 | 70 | @implementer(ISnapFile) | 78 | @implementer(ISnapFile) |
670 | 71 | class SnapFile(Storm): | 79 | class SnapFile(Storm): |
671 | 72 | """See `ISnap`.""" | 80 | """See `ISnap`.""" |
672 | @@ -300,6 +308,16 @@ | |||
673 | 300 | """See `IPackageBuild`.""" | 308 | """See `IPackageBuild`.""" |
674 | 301 | return not self.getFiles().is_empty() | 309 | return not self.getFiles().is_empty() |
675 | 302 | 310 | ||
676 | 311 | def updateStatus(self, status, builder=None, slave_status=None, | ||
677 | 312 | date_started=None, date_finished=None, | ||
678 | 313 | force_invalid_transition=False): | ||
679 | 314 | """See `IBuildFarmJob`.""" | ||
680 | 315 | super(SnapBuild, self).updateStatus( | ||
681 | 316 | status, builder=builder, slave_status=slave_status, | ||
682 | 317 | date_started=date_started, date_finished=date_finished, | ||
683 | 318 | force_invalid_transition=force_invalid_transition) | ||
684 | 319 | notify(SnapBuildStatusChangedEvent(self)) | ||
685 | 320 | |||
686 | 303 | def notify(self, extra_info=None): | 321 | def notify(self, extra_info=None): |
687 | 304 | """See `IPackageBuild`.""" | 322 | """See `IPackageBuild`.""" |
688 | 305 | if not config.builddmaster.send_build_notification: | 323 | if not config.builddmaster.send_build_notification: |
689 | 306 | 324 | ||
690 | === added directory 'lib/lp/snappy/subscribers' | |||
691 | === added file 'lib/lp/snappy/subscribers/__init__.py' | |||
692 | === added file 'lib/lp/snappy/subscribers/snapbuild.py' | |||
693 | --- lib/lp/snappy/subscribers/snapbuild.py 1970-01-01 00:00:00 +0000 | |||
694 | +++ lib/lp/snappy/subscribers/snapbuild.py 2016-02-19 14:20:42 +0000 | |||
695 | @@ -0,0 +1,30 @@ | |||
696 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
697 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
698 | 3 | |||
699 | 4 | """Event subscribers for snap builds.""" | ||
700 | 5 | |||
701 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
702 | 7 | |||
703 | 8 | __metaclass__ = type | ||
704 | 9 | |||
705 | 10 | from zope.component import getUtility | ||
706 | 11 | |||
707 | 12 | from lp.services.features import getFeatureFlag | ||
708 | 13 | from lp.services.webapp.publisher import canonical_url | ||
709 | 14 | from lp.services.webhooks.interfaces import IWebhookSet | ||
710 | 15 | from lp.services.webhooks.payload import compose_webhook_payload | ||
711 | 16 | from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG | ||
712 | 17 | from lp.snappy.interfaces.snapbuild import ISnapBuild | ||
713 | 18 | |||
714 | 19 | |||
715 | 20 | def snap_build_status_changed(snapbuild, event): | ||
716 | 21 | """Trigger webhooks when snap package build statuses change.""" | ||
717 | 22 | if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG): | ||
718 | 23 | payload = { | ||
719 | 24 | "snap_build": canonical_url(snapbuild, force_local_path=True), | ||
720 | 25 | "action": "status-changed", | ||
721 | 26 | } | ||
722 | 27 | payload.update(compose_webhook_payload( | ||
723 | 28 | ISnapBuild, snapbuild, ["snap", "status"])) | ||
724 | 29 | getUtility(IWebhookSet).trigger( | ||
725 | 30 | snapbuild.snap, "snap:build:0.1", payload) | ||
726 | 0 | 31 | ||
727 | === modified file 'lib/lp/snappy/tests/test_snap.py' | |||
728 | --- lib/lp/snappy/tests/test_snap.py 2016-02-05 06:05:50 +0000 | |||
729 | +++ lib/lp/snappy/tests/test_snap.py 2016-02-19 14:20:42 +0000 | |||
730 | @@ -1,4 +1,4 @@ | |||
732 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
733 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
734 | 3 | 3 | ||
735 | 4 | """Test snap packages.""" | 4 | """Test snap packages.""" |
736 | @@ -8,6 +8,7 @@ | |||
737 | 8 | from datetime import timedelta | 8 | from datetime import timedelta |
738 | 9 | 9 | ||
739 | 10 | from lazr.lifecycle.event import ObjectModifiedEvent | 10 | from lazr.lifecycle.event import ObjectModifiedEvent |
740 | 11 | from storm.exceptions import LostObjectError | ||
741 | 11 | from storm.locals import Store | 12 | from storm.locals import Store |
742 | 12 | from testtools.matchers import Equals | 13 | from testtools.matchers import Equals |
743 | 13 | import transaction | 14 | import transaction |
744 | @@ -371,6 +372,16 @@ | |||
745 | 371 | self.assertRaises(CannotDeleteSnap, snap.destroySelf) | 372 | self.assertRaises(CannotDeleteSnap, snap.destroySelf) |
746 | 372 | self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned")) | 373 | self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned")) |
747 | 373 | 374 | ||
748 | 375 | def test_related_webhooks_deleted(self): | ||
749 | 376 | owner = self.factory.makePerson() | ||
750 | 377 | snap = self.factory.makeSnap(registrant=owner, owner=owner) | ||
751 | 378 | webhook = self.factory.makeWebhook(target=snap) | ||
752 | 379 | with person_logged_in(snap.owner): | ||
753 | 380 | webhook.ping() | ||
754 | 381 | snap.destroySelf() | ||
755 | 382 | transaction.commit() | ||
756 | 383 | self.assertRaises(LostObjectError, getattr, webhook, "target") | ||
757 | 384 | |||
758 | 374 | 385 | ||
759 | 375 | class TestSnapSet(TestCaseWithFactory): | 386 | class TestSnapSet(TestCaseWithFactory): |
760 | 376 | 387 | ||
761 | 377 | 388 | ||
762 | === modified file 'lib/lp/snappy/tests/test_snapbuild.py' | |||
763 | --- lib/lp/snappy/tests/test_snapbuild.py 2016-02-06 00:12:30 +0000 | |||
764 | +++ lib/lp/snappy/tests/test_snapbuild.py 2016-02-19 14:20:42 +0000 | |||
765 | @@ -15,6 +15,11 @@ | |||
766 | 15 | ) | 15 | ) |
767 | 16 | 16 | ||
768 | 17 | import pytz | 17 | import pytz |
769 | 18 | from testtools.matchers import ( | ||
770 | 19 | Equals, | ||
771 | 20 | MatchesDict, | ||
772 | 21 | MatchesStructure, | ||
773 | 22 | ) | ||
774 | 18 | from zope.component import getUtility | 23 | from zope.component import getUtility |
775 | 19 | from zope.security.proxy import removeSecurityProxy | 24 | from zope.security.proxy import removeSecurityProxy |
776 | 20 | 25 | ||
777 | @@ -29,6 +34,7 @@ | |||
778 | 29 | from lp.services.features.testing import FeatureFixture | 34 | from lp.services.features.testing import FeatureFixture |
779 | 30 | from lp.services.librarian.browser import ProxiedLibraryFileAlias | 35 | from lp.services.librarian.browser import ProxiedLibraryFileAlias |
780 | 31 | from lp.services.webapp.interfaces import OAuthPermission | 36 | from lp.services.webapp.interfaces import OAuthPermission |
781 | 37 | from lp.services.webapp.publisher import canonical_url | ||
782 | 32 | from lp.snappy.interfaces.snap import ( | 38 | from lp.snappy.interfaces.snap import ( |
783 | 33 | SNAP_TESTING_FLAGS, | 39 | SNAP_TESTING_FLAGS, |
784 | 34 | SnapFeatureDisabled, | 40 | SnapFeatureDisabled, |
785 | @@ -218,6 +224,25 @@ | |||
786 | 218 | self.factory.makeSnapFile(snapbuild=self.build) | 224 | self.factory.makeSnapFile(snapbuild=self.build) |
787 | 219 | self.assertTrue(self.build.verifySuccessfulUpload()) | 225 | self.assertTrue(self.build.verifySuccessfulUpload()) |
788 | 220 | 226 | ||
789 | 227 | def test_updateStatus_triggers_webhooks(self): | ||
790 | 228 | # Updating the status of a SnapBuild triggers webhooks on the | ||
791 | 229 | # corresponding Snap. | ||
792 | 230 | hook = self.factory.makeWebhook( | ||
793 | 231 | target=self.build.snap, event_types=["snap:build:0.1"]) | ||
794 | 232 | self.build.updateStatus(BuildStatus.FULLYBUILT) | ||
795 | 233 | expected_payload = { | ||
796 | 234 | "snap_build": Equals( | ||
797 | 235 | canonical_url(self.build, force_local_path=True)), | ||
798 | 236 | "action": Equals("status-changed"), | ||
799 | 237 | "snap": Equals( | ||
800 | 238 | canonical_url(self.build.snap, force_local_path=True)), | ||
801 | 239 | "status": Equals("Successfully built"), | ||
802 | 240 | } | ||
803 | 241 | self.assertThat( | ||
804 | 242 | hook.deliveries.one(), MatchesStructure( | ||
805 | 243 | event_type=Equals("snap:build:0.1"), | ||
806 | 244 | payload=MatchesDict(expected_payload))) | ||
807 | 245 | |||
808 | 221 | def test_notify_fullybuilt(self): | 246 | def test_notify_fullybuilt(self): |
809 | 222 | # notify does not send mail when a SnapBuild completes normally. | 247 | # notify does not send mail when a SnapBuild completes normally. |
810 | 223 | person = self.factory.makePerson(name="person") | 248 | person = self.factory.makePerson(name="person") |
Thanks, Colin.
Regarding the DB security changes, do we need to deploy them specially or are they applied for every devel revision on-the-fly ?
The new webhook payload looks complete (action, snap_url, snap_build_url, status) and useful for API operations.
Obviously I'm missing tons of important details, but the drive-by refactoring make sense and testing looks sufficient.
[]