Merge lp:~wgrant/launchpad/webhook-deletion into lp:launchpad
- webhook-deletion
- Merge into devel
Proposed by
William Grant
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17643 | ||||
Proposed branch: | lp:~wgrant/launchpad/webhook-deletion | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
547 lines (+196/-28) 11 files modified
database/schema/security.cfg (+1/-0) lib/lp/code/model/gitrepository.py (+2/-0) lib/lp/code/model/tests/test_gitrepository.py (+15/-9) lib/lp/scripts/garbo.py (+35/-10) lib/lp/scripts/tests/test_garbo.py (+20/-1) lib/lp/services/webhooks/configure.zcml (+5/-0) lib/lp/services/webhooks/interfaces.py (+16/-0) lib/lp/services/webhooks/model.py (+25/-0) lib/lp/services/webhooks/tests/test_webhook.py (+25/-7) lib/lp/services/webhooks/tests/test_webhookjob.py (+42/-1) lib/lp/services/webhooks/tests/test_webservice.py (+10/-0) |
||||
To merge this branch: | bzr merge lp:~wgrant/launchpad/webhook-deletion | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+265360@code.launchpad.net |
Commit message
Allow Webhooks to be deleted through the API, and prune WebhookJobs older than a month.
Description of the change
Allow Webhooks to be deleted through the API, and prune WebhookJobs older than a month.
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
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-07-21 10:51:36 +0000 |
3 | +++ database/schema/security.cfg 2015-07-22 07:10:43 +0000 |
4 | @@ -2301,6 +2301,7 @@ |
5 | public.teamparticipation = SELECT, DELETE |
6 | public.translationmessage = SELECT, DELETE |
7 | public.translationtemplateitem = SELECT, DELETE |
8 | +public.webhookjob = SELECT, DELETE |
9 | type=user |
10 | |
11 | [garbo_daily] |
12 | |
13 | === modified file 'lib/lp/code/model/gitrepository.py' |
14 | --- lib/lp/code/model/gitrepository.py 2015-07-15 06:25:36 +0000 |
15 | +++ lib/lp/code/model/gitrepository.py 2015-07-22 07:10:43 +0000 |
16 | @@ -146,6 +146,7 @@ |
17 | get_property_cache, |
18 | ) |
19 | from lp.services.webapp.authorization import available_with_permission |
20 | +from lp.services.webhooks.interfaces import IWebhookSource |
21 | from lp.services.webhooks.model import WebhookTargetMixin |
22 | |
23 | |
24 | @@ -1075,6 +1076,7 @@ |
25 | self._deleteRepositoryAccessGrants() |
26 | self._deleteRepositorySubscriptions() |
27 | self._deleteJobs() |
28 | + getUtility(IWebhookSource).delete(self.webhooks) |
29 | |
30 | # Now destroy the repository. |
31 | repository_name = self.unique_name |
32 | |
33 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
34 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-07-10 22:24:49 +0000 |
35 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-07-22 07:10:43 +0000 |
36 | @@ -16,6 +16,7 @@ |
37 | from lazr.lifecycle.snapshot import Snapshot |
38 | import pytz |
39 | from sqlobject import SQLObjectNotFound |
40 | +from storm.exceptions import LostObjectError |
41 | from storm.store import Store |
42 | from testtools.matchers import ( |
43 | EndsWith, |
44 | @@ -321,7 +322,7 @@ |
45 | class TestGitRepositoryDeletion(TestCaseWithFactory): |
46 | """Test the different cases that make a repository deletable or not.""" |
47 | |
48 | - layer = LaunchpadZopelessLayer |
49 | + layer = LaunchpadFunctionalLayer |
50 | |
51 | def setUp(self): |
52 | super(TestGitRepositoryDeletion, self).setUp() |
53 | @@ -414,10 +415,9 @@ |
54 | |
55 | def test_related_GitJobs_deleted(self): |
56 | # A repository with an associated job will delete those jobs. |
57 | - repository = self.factory.makeGitRepository() |
58 | - GitAPI(None, None).notify(repository.getInternalPath()) |
59 | - store = Store.of(repository) |
60 | - repository.destroySelf() |
61 | + GitAPI(None, None).notify(self.repository.getInternalPath()) |
62 | + store = Store.of(self.repository) |
63 | + self.repository.destroySelf() |
64 | # Need to commit the transaction to fire off the constraint checks. |
65 | transaction.commit() |
66 | jobs = store.find(GitJob, GitJob.job_type == GitJobType.REF_SCAN) |
67 | @@ -426,10 +426,9 @@ |
68 | def test_creates_job_to_reclaim_space(self): |
69 | # When a repository is deleted from the database, a job is created |
70 | # to remove the repository from disk as well. |
71 | - repository = self.factory.makeGitRepository() |
72 | - repository_path = repository.getInternalPath() |
73 | - store = Store.of(repository) |
74 | - repository.destroySelf() |
75 | + repository_path = self.repository.getInternalPath() |
76 | + store = Store.of(self.repository) |
77 | + self.repository.destroySelf() |
78 | jobs = store.find( |
79 | GitJob, |
80 | GitJob.job_type == GitJobType.RECLAIM_REPOSITORY_SPACE) |
81 | @@ -468,6 +467,13 @@ |
82 | ) |
83 | self.repository.destroySelf(break_references=True) |
84 | |
85 | + def test_related_webhooks_deleted(self): |
86 | + webhook = self.factory.makeWebhook(target=self.repository) |
87 | + webhook.ping() |
88 | + self.repository.destroySelf() |
89 | + transaction.commit() |
90 | + self.assertRaises(LostObjectError, getattr, webhook, 'target') |
91 | + |
92 | |
93 | class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): |
94 | """Test determination and application of repository deletion |
95 | |
96 | === modified file 'lib/lp/scripts/garbo.py' |
97 | --- lib/lp/scripts/garbo.py 2015-07-21 08:42:30 +0000 |
98 | +++ lib/lp/scripts/garbo.py 2015-07-22 07:10:43 +0000 |
99 | @@ -119,6 +119,8 @@ |
100 | ) |
101 | from lp.services.session.model import SessionData |
102 | from lp.services.verification.model.logintoken import LoginToken |
103 | +from lp.services.webhooks.interfaces import IWebhookJobSource |
104 | +from lp.services.webhooks.model import WebhookJob |
105 | from lp.soyuz.model.archive import Archive |
106 | from lp.soyuz.model.livefsbuild import LiveFSFile |
107 | from lp.soyuz.model.publishing import SourcePackagePublishingHistory |
108 | @@ -1112,6 +1114,28 @@ |
109 | """ |
110 | |
111 | |
112 | +class WebhookJobPruner(TunableLoop): |
113 | + """Prune `WebhookJobs` that finished more than a month ago.""" |
114 | + |
115 | + maximum_chunk_size = 5000 |
116 | + |
117 | + @property |
118 | + def old_jobs(self): |
119 | + return IMasterStore(WebhookJob).using(WebhookJob, Job).find( |
120 | + (WebhookJob.job_id,), |
121 | + Job.id == WebhookJob.job_id, |
122 | + Job.date_finished < SQL( |
123 | + "CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - '30 days'::interval")) |
124 | + |
125 | + def __call__(self, chunksize): |
126 | + getUtility(IWebhookJobSource).deleteByIDs( |
127 | + list(self.old_jobs[:int(chunksize)].values(WebhookJob.job_id))) |
128 | + transaction.commit() |
129 | + |
130 | + def isDone(self): |
131 | + return self.old_jobs.is_empty() |
132 | + |
133 | + |
134 | class BugHeatUpdater(TunableLoop): |
135 | """A `TunableLoop` for bug heat calculations.""" |
136 | |
137 | @@ -1663,12 +1687,12 @@ |
138 | """ |
139 | script_name = 'garbo-frequently' |
140 | tunable_loops = [ |
141 | + AntiqueSessionPruner, |
142 | BugSummaryJournalRollup, |
143 | + OpenIDConsumerAssociationPruner, |
144 | OpenIDConsumerNoncePruner, |
145 | - OpenIDConsumerAssociationPruner, |
146 | - AntiqueSessionPruner, |
147 | + PopulateLatestPersonSourcePackageReleaseCache, |
148 | VoucherRedeemer, |
149 | - PopulateLatestPersonSourcePackageReleaseCache, |
150 | ] |
151 | experimental_tunable_loops = [] |
152 | |
153 | @@ -1685,11 +1709,11 @@ |
154 | """ |
155 | script_name = 'garbo-hourly' |
156 | tunable_loops = [ |
157 | + BugHeatUpdater, |
158 | + BugWatchScheduler, |
159 | + DuplicateSessionPruner, |
160 | RevisionCachePruner, |
161 | - BugWatchScheduler, |
162 | UnusedSessionPruner, |
163 | - DuplicateSessionPruner, |
164 | - BugHeatUpdater, |
165 | ] |
166 | experimental_tunable_loops = [] |
167 | |
168 | @@ -1714,6 +1738,7 @@ |
169 | BugWatchActivityPruner, |
170 | CodeImportEventPruner, |
171 | CodeImportResultPruner, |
172 | + DiffPruner, |
173 | GitJobPruner, |
174 | HWSubmissionEmailLinker, |
175 | LiveFSFilePruner, |
176 | @@ -1721,17 +1746,17 @@ |
177 | ObsoleteBugAttachmentPruner, |
178 | OldTimeLimitedTokenDeleter, |
179 | PersonSettingsENFPopulator, |
180 | + POTranslationPruner, |
181 | + PreviewDiffPruner, |
182 | + ProductVCSPopulator, |
183 | RevisionAuthorEmailLinker, |
184 | ScrubPOFileTranslator, |
185 | SuggestiveTemplatesCacheUpdater, |
186 | TeamMembershipPruner, |
187 | - POTranslationPruner, |
188 | UnlinkedAccountPruner, |
189 | UnusedAccessPolicyPruner, |
190 | UnusedPOTMsgSetPruner, |
191 | - ProductVCSPopulator, |
192 | - PreviewDiffPruner, |
193 | - DiffPruner, |
194 | + WebhookJobPruner, |
195 | ] |
196 | experimental_tunable_loops = [ |
197 | PersonPruner, |
198 | |
199 | === modified file 'lib/lp/scripts/tests/test_garbo.py' |
200 | --- lib/lp/scripts/tests/test_garbo.py 2015-07-21 08:42:30 +0000 |
201 | +++ lib/lp/scripts/tests/test_garbo.py 2015-07-22 07:10:43 +0000 |
202 | @@ -16,7 +16,10 @@ |
203 | import time |
204 | |
205 | from pytz import UTC |
206 | -from storm.exceptions import NoneError |
207 | +from storm.exceptions import ( |
208 | + LostObjectError, |
209 | + NoneError, |
210 | + ) |
211 | from storm.expr import ( |
212 | In, |
213 | Like, |
214 | @@ -979,6 +982,22 @@ |
215 | switch_dbuser('testadmin') |
216 | self.assertEqual(1, store.find(GitJob).count()) |
217 | |
218 | + def test_WebhookJobPruner(self): |
219 | + # Garbo should remove jobs completed over 30 days ago. |
220 | + switch_dbuser('testadmin') |
221 | + |
222 | + webhook = self.factory.makeWebhook() |
223 | + job1 = webhook.ping() |
224 | + removeSecurityProxy(job1).job.date_finished = THIRTY_DAYS_AGO |
225 | + job2 = webhook.ping() |
226 | + removeSecurityProxy(job2).job.date_finished = SEVEN_DAYS_AGO |
227 | + |
228 | + self.runDaily() |
229 | + |
230 | + switch_dbuser('testadmin') |
231 | + self.assertEqual(webhook, job2.webhook) |
232 | + self.assertRaises(LostObjectError, getattr, job1, 'webhook') |
233 | + |
234 | def test_ObsoleteBugAttachmentPruner(self): |
235 | # Bug attachments without a LibraryFileContent record are removed. |
236 | |
237 | |
238 | === modified file 'lib/lp/services/webhooks/configure.zcml' |
239 | --- lib/lp/services/webhooks/configure.zcml 2015-07-13 09:13:57 +0000 |
240 | +++ lib/lp/services/webhooks/configure.zcml 2015-07-22 07:10:43 +0000 |
241 | @@ -23,6 +23,11 @@ |
242 | </securedutility> |
243 | |
244 | <securedutility |
245 | + component="lp.services.webhooks.model.WebhookJob" |
246 | + provides="lp.services.webhooks.interfaces.IWebhookJobSource"> |
247 | + <allow interface="lp.services.webhooks.interfaces.IWebhookJobSource"/> |
248 | + </securedutility> |
249 | + <securedutility |
250 | component="lp.services.webhooks.model.WebhookDeliveryJob" |
251 | provides="lp.services.webhooks.interfaces.IWebhookDeliveryJobSource"> |
252 | <allow interface="lp.services.webhooks.interfaces.IWebhookDeliveryJobSource"/> |
253 | |
254 | === modified file 'lib/lp/services/webhooks/interfaces.py' |
255 | --- lib/lp/services/webhooks/interfaces.py 2015-07-07 07:37:54 +0000 |
256 | +++ lib/lp/services/webhooks/interfaces.py 2015-07-22 07:10:43 +0000 |
257 | @@ -11,6 +11,7 @@ |
258 | 'IWebhookDeliveryJob', |
259 | 'IWebhookDeliveryJobSource', |
260 | 'IWebhookJob', |
261 | + 'IWebhookJobSource', |
262 | 'IWebhookSource', |
263 | 'IWebhookTarget', |
264 | 'WebhookFeatureDisabled', |
265 | @@ -23,6 +24,7 @@ |
266 | call_with, |
267 | error_status, |
268 | export_as_webservice_entry, |
269 | + export_destructor_operation, |
270 | export_factory_operation, |
271 | exported, |
272 | operation_for_version, |
273 | @@ -112,6 +114,11 @@ |
274 | def ping(): |
275 | """Send a test event.""" |
276 | |
277 | + @export_destructor_operation() |
278 | + @operation_for_version('devel') |
279 | + def destroySelf(): |
280 | + """Delete this webhook.""" |
281 | + |
282 | |
283 | class IWebhookSource(Interface): |
284 | |
285 | @@ -159,6 +166,15 @@ |
286 | json_data = Attribute(_("A dict of data about the job.")) |
287 | |
288 | |
289 | +class IWebhookJobSource(IJobSource): |
290 | + |
291 | + def deleteByIDs(webhookjob_ids): |
292 | + """Delete `IWebhookJob`s by their primary key (`Job.id`).""" |
293 | + |
294 | + def deleteByWebhooks(webhooks): |
295 | + """Delete all `IWebhookJob`s for the given `IWebhook`.""" |
296 | + |
297 | + |
298 | class IWebhookDeliveryJob(IRunnableJob): |
299 | """A Job that delivers an event to a webhook consumer.""" |
300 | |
301 | |
302 | === modified file 'lib/lp/services/webhooks/model.py' |
303 | --- lib/lp/services/webhooks/model.py 2015-07-13 09:13:57 +0000 |
304 | +++ lib/lp/services/webhooks/model.py 2015-07-22 07:10:43 +0000 |
305 | @@ -58,6 +58,7 @@ |
306 | IWebhookDeliveryJob, |
307 | IWebhookDeliveryJobSource, |
308 | IWebhookJob, |
309 | + IWebhookJobSource, |
310 | IWebhookSource, |
311 | WebhookFeatureDisabled, |
312 | ) |
313 | @@ -122,6 +123,9 @@ |
314 | def ping(self): |
315 | return WebhookDeliveryJob.create(self, {'ping': True}) |
316 | |
317 | + def destroySelf(self): |
318 | + getUtility(IWebhookSource).delete([self]) |
319 | + |
320 | @property |
321 | def event_types(self): |
322 | return (self.json_data or {}).get('event_types', []) |
323 | @@ -157,6 +161,8 @@ |
324 | return hook |
325 | |
326 | def delete(self, hooks): |
327 | + hooks = list(hooks) |
328 | + getUtility(IWebhookJobSource).deleteByWebhooks(hooks) |
329 | IStore(Webhook).find( |
330 | Webhook, Webhook.id.is_in(set(hook.id for hook in hooks))).remove() |
331 | |
332 | @@ -200,6 +206,7 @@ |
333 | """) |
334 | |
335 | |
336 | +@provider(IWebhookJobSource) |
337 | @implementer(IWebhookJob) |
338 | class WebhookJob(StormBase): |
339 | """See `IWebhookJob`.""" |
340 | @@ -236,6 +243,24 @@ |
341 | def makeDerived(self): |
342 | return WebhookJobDerived.makeSubclass(self) |
343 | |
344 | + @staticmethod |
345 | + def deleteByIDs(webhookjob_ids): |
346 | + """See `IWebhookJobSource`.""" |
347 | + # Assumes that Webhook's PK is its FK to Job.id. |
348 | + webookjob_ids = list(webhookjob_ids) |
349 | + IStore(WebhookJob).find( |
350 | + WebhookJob, WebhookJob.job_id.is_in(webhookjob_ids)).remove() |
351 | + IStore(Job).find(Job, Job.id.is_in(webhookjob_ids)).remove() |
352 | + |
353 | + @classmethod |
354 | + def deleteByWebhooks(cls, webhooks): |
355 | + """See `IWebhookJobSource`.""" |
356 | + result = IStore(WebhookJob).find( |
357 | + WebhookJob, |
358 | + WebhookJob.webhook_id.is_in(hook.id for hook in webhooks)) |
359 | + job_ids = list(result.values(WebhookJob.job_id)) |
360 | + cls.deleteByIDs(job_ids) |
361 | + |
362 | |
363 | @delegate_to(IWebhookJob) |
364 | class WebhookJobDerived(BaseRunnableJob): |
365 | |
366 | === modified file 'lib/lp/services/webhooks/tests/test_webhook.py' |
367 | --- lib/lp/services/webhooks/tests/test_webhook.py 2015-07-12 23:51:16 +0000 |
368 | +++ lib/lp/services/webhooks/tests/test_webhook.py 2015-07-22 07:10:43 +0000 |
369 | @@ -3,25 +3,32 @@ |
370 | |
371 | from lazr.lifecycle.event import ObjectModifiedEvent |
372 | from storm.store import Store |
373 | -from testtools.matchers import GreaterThan |
374 | +from testtools.matchers import ( |
375 | + Equals, |
376 | + GreaterThan, |
377 | + ) |
378 | import transaction |
379 | from zope.component import getUtility |
380 | from zope.event import notify |
381 | from zope.security.checker import getChecker |
382 | |
383 | +from lp.services.database.interfaces import IStore |
384 | from lp.services.webapp.authorization import check_permission |
385 | from lp.services.webhooks.interfaces import ( |
386 | IWebhook, |
387 | IWebhookSource, |
388 | ) |
389 | +from lp.services.webhooks.model import WebhookJob |
390 | from lp.testing import ( |
391 | admin_logged_in, |
392 | anonymous_logged_in, |
393 | login_person, |
394 | person_logged_in, |
395 | + StormStatementRecorder, |
396 | TestCaseWithFactory, |
397 | ) |
398 | from lp.testing.layers import DatabaseFunctionalLayer |
399 | +from lp.testing.matchers import HasQueryCount |
400 | |
401 | |
402 | class TestWebhook(TestCaseWithFactory): |
403 | @@ -67,8 +74,9 @@ |
404 | expected_get_permissions = { |
405 | 'launchpad.View': set(( |
406 | 'active', 'date_created', 'date_last_modified', 'deliveries', |
407 | - 'delivery_url', 'event_types', 'getDelivery', 'id', 'ping', |
408 | - 'registrant', 'registrant_id', 'secret', 'target')), |
409 | + 'delivery_url', 'destroySelf', 'event_types', 'getDelivery', |
410 | + 'id', 'ping', 'registrant', 'registrant_id', 'secret', |
411 | + 'target')), |
412 | } |
413 | webhook = self.factory.makeWebhook() |
414 | checker = getChecker(webhook) |
415 | @@ -141,15 +149,25 @@ |
416 | def test_delete(self): |
417 | target = self.factory.makeGitRepository() |
418 | login_person(target.owner) |
419 | - hooks = [ |
420 | - self.factory.makeWebhook(target, u'http://path/to/%d' % i) |
421 | - for i in range(3)] |
422 | + hooks = [] |
423 | + for i in range(3): |
424 | + hook = self.factory.makeWebhook(target, u'http://path/to/%d' % i) |
425 | + hook.ping() |
426 | + hooks.append(hook) |
427 | + self.assertEqual(3, IStore(WebhookJob).find(WebhookJob).count()) |
428 | self.assertContentEqual( |
429 | [u'http://path/to/0', u'http://path/to/1', u'http://path/to/2'], |
430 | [hook.delivery_url for hook in |
431 | getUtility(IWebhookSource).findByTarget(target)]) |
432 | - getUtility(IWebhookSource).delete(hooks[:2]) |
433 | + |
434 | + transaction.commit() |
435 | + with StormStatementRecorder() as recorder: |
436 | + getUtility(IWebhookSource).delete(hooks[:2]) |
437 | + self.assertThat(recorder, HasQueryCount(Equals(4))) |
438 | + |
439 | self.assertContentEqual( |
440 | [u'http://path/to/2'], |
441 | [hook.delivery_url for hook in |
442 | getUtility(IWebhookSource).findByTarget(target)]) |
443 | + self.assertEqual(1, IStore(WebhookJob).find(WebhookJob).count()) |
444 | + self.assertEqual(1, hooks[2].deliveries.count()) |
445 | |
446 | === modified file 'lib/lp/services/webhooks/tests/test_webhookjob.py' |
447 | --- lib/lp/services/webhooks/tests/test_webhookjob.py 2015-07-13 11:20:08 +0000 |
448 | +++ lib/lp/services/webhooks/tests/test_webhookjob.py 2015-07-22 07:10:43 +0000 |
449 | @@ -10,6 +10,7 @@ |
450 | urlmatch, |
451 | ) |
452 | import requests |
453 | +from storm.store import Store |
454 | from testtools import TestCase |
455 | from testtools.matchers import ( |
456 | Contains, |
457 | @@ -22,6 +23,7 @@ |
458 | Not, |
459 | ) |
460 | import transaction |
461 | +from zope.component import getUtility |
462 | |
463 | from lp.services.features.testing import FeatureFixture |
464 | from lp.services.job.interfaces.job import JobStatus |
465 | @@ -33,6 +35,7 @@ |
466 | IWebhookClient, |
467 | IWebhookDeliveryJob, |
468 | IWebhookJob, |
469 | + IWebhookJobSource, |
470 | ) |
471 | from lp.services.webhooks.model import ( |
472 | WebhookDeliveryJob, |
473 | @@ -40,7 +43,10 @@ |
474 | WebhookJobDerived, |
475 | WebhookJobType, |
476 | ) |
477 | -from lp.testing import TestCaseWithFactory |
478 | +from lp.testing import ( |
479 | + login_person, |
480 | + TestCaseWithFactory, |
481 | + ) |
482 | from lp.testing.dbuser import dbuser |
483 | from lp.testing.fixture import ( |
484 | CaptureOops, |
485 | @@ -65,6 +71,41 @@ |
486 | WebhookJob(hook, WebhookJobType.DELIVERY, {}), IWebhookJob) |
487 | |
488 | |
489 | +class TestWebhookJobSource(TestCaseWithFactory): |
490 | + |
491 | + layer = DatabaseFunctionalLayer |
492 | + |
493 | + def test_deleteByIDs(self): |
494 | + target = self.factory.makeGitRepository() |
495 | + login_person(target.owner) |
496 | + hook = self.factory.makeWebhook(target=target) |
497 | + job1 = hook.ping() |
498 | + job2 = hook.ping() |
499 | + job3 = hook.ping() |
500 | + self.assertContentEqual([job3, job2, job1], hook.deliveries) |
501 | + getUtility(IWebhookJobSource).deleteByIDs([job1.job_id, job3.job_id]) |
502 | + self.assertContentEqual([job2], hook.deliveries) |
503 | + |
504 | + def test_deleteByWebhooks(self): |
505 | + target = self.factory.makeGitRepository() |
506 | + login_person(target.owner) |
507 | + hook1 = self.factory.makeWebhook(target=target) |
508 | + job1 = hook1.ping() |
509 | + job2 = hook1.ping() |
510 | + hook2 = self.factory.makeWebhook(target=target) |
511 | + job3 = hook2.ping() |
512 | + hook3 = self.factory.makeWebhook(target=target) |
513 | + job4 = hook3.ping() |
514 | + store = Store.of(hook1) |
515 | + self.assertEqual(4, store.find(WebhookJob).count()) |
516 | + self.assertContentEqual([job2, job1], hook1.deliveries) |
517 | + self.assertContentEqual([job3], hook2.deliveries) |
518 | + self.assertContentEqual([job4], hook3.deliveries) |
519 | + getUtility(IWebhookJobSource).deleteByWebhooks([hook1, hook2]) |
520 | + self.assertEqual(1, store.find(WebhookJob).count()) |
521 | + self.assertContentEqual([job4], hook3.deliveries) |
522 | + |
523 | + |
524 | class TestWebhookJobDerived(TestCaseWithFactory): |
525 | """Tests for `WebhookJobDerived`.""" |
526 | |
527 | |
528 | === modified file 'lib/lp/services/webhooks/tests/test_webservice.py' |
529 | --- lib/lp/services/webhooks/tests/test_webservice.py 2015-07-07 07:37:54 +0000 |
530 | +++ lib/lp/services/webhooks/tests/test_webservice.py 2015-07-22 07:10:43 +0000 |
531 | @@ -126,6 +126,16 @@ |
532 | get_deliveries, create_delivery, 2) |
533 | self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count))) |
534 | |
535 | + def test_delete(self): |
536 | + with person_logged_in(self.owner): |
537 | + self.webhook.ping() |
538 | + delete_response = self.webservice.delete( |
539 | + self.webhook_url, api_version='devel') |
540 | + self.assertEqual(200, delete_response.status) |
541 | + get_response = self.webservice.get( |
542 | + self.webhook_url, api_version='devel') |
543 | + self.assertEqual(404, get_response.status) |
544 | + |
545 | |
546 | class TestWebhookDelivery(TestCaseWithFactory): |
547 | layer = DatabaseFunctionalLayer |