Merge lp:~cjwatson/launchpad/bzr-webhooks into lp:launchpad
- bzr-webhooks
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17771 | ||||
Proposed branch: | lp:~cjwatson/launchpad/bzr-webhooks | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/db-bzr-webhooks | ||||
Diff against target: |
1669 lines (+563/-321) 24 files modified
lib/lp/code/browser/branch.py (+11/-2) lib/lp/code/configure.zcml (+3/-0) lib/lp/code/interfaces/branch.py (+48/-18) lib/lp/code/interfaces/branchlookup.py (+8/-0) lib/lp/code/interfaces/branchtarget.py (+1/-1) lib/lp/code/interfaces/gitrepository.py (+2/-1) lib/lp/code/model/branch.py (+8/-1) lib/lp/code/model/branchlookup.py (+11/-8) lib/lp/code/model/branchtarget.py (+3/-3) lib/lp/code/model/tests/test_branch.py (+206/-206) lib/lp/code/model/tests/test_branchlookup.py (+35/-30) lib/lp/code/model/tests/test_branchset.py (+12/-0) lib/lp/code/model/tests/test_branchtarget.py (+9/-9) lib/lp/codehosting/scanner/bzrsync.py (+13/-1) lib/lp/codehosting/scanner/events.py (+9/-1) lib/lp/codehosting/scanner/tests/test_bzrsync.py (+33/-1) lib/lp/registry/browser/productseries.py (+2/-1) lib/lp/services/webhooks/client.py (+1/-1) lib/lp/services/webhooks/interfaces.py (+2/-1) lib/lp/services/webhooks/model.py (+11/-0) lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt (+2/-2) lib/lp/services/webhooks/tests/test_browser.py (+73/-8) lib/lp/services/webhooks/tests/test_model.py (+31/-15) lib/lp/services/webhooks/tests/test_webservice.py (+29/-11) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/bzr-webhooks | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+272248@code.launchpad.net |
Commit message
Add webhook support for Bazaar branches.
Description of the change
Add webhook support for Bazaar branches. It's almost exactly the same shape as for Git repositories, but with the webhook triggering hooked into the TipChange event instead of written directly in the job, and with a slightly different payload structure since there's only one pair of revisions involved.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
William Grant (wgrant) wrote : | # |
Actually, is there any way to get a branch by its shortened_name? lp.branches.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/browser/branch.py' |
2 | --- lib/lp/code/browser/branch.py 2015-09-23 15:38:13 +0000 |
3 | +++ lib/lp/code/browser/branch.py 2015-09-29 15:54:57 +0000 |
4 | @@ -125,6 +125,7 @@ |
5 | from lp.services import searchbuilder |
6 | from lp.services.config import config |
7 | from lp.services.database.constants import UTC_NOW |
8 | +from lp.services.features import getFeatureFlag |
9 | from lp.services.feeds.browser import ( |
10 | BranchFeedLink, |
11 | FeedsMixin, |
12 | @@ -152,6 +153,7 @@ |
13 | from lp.services.webapp.breadcrumb import NameBreadcrumb |
14 | from lp.services.webapp.escaping import structured |
15 | from lp.services.webapp.interfaces import ICanonicalUrlData |
16 | +from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
17 | from lp.snappy.browser.hassnaps import ( |
18 | HasSnapsMenuMixin, |
19 | HasSnapsViewMixin, |
20 | @@ -183,7 +185,7 @@ |
21 | return self.context.target.components[-1] |
22 | |
23 | |
24 | -class BranchNavigation(Navigation): |
25 | +class BranchNavigation(WebhookTargetNavigationMixin, Navigation): |
26 | |
27 | usedfor = IBranch |
28 | |
29 | @@ -240,7 +242,7 @@ |
30 | facet = 'branches' |
31 | title = 'Edit branch' |
32 | links = ( |
33 | - 'edit', 'reviewer', 'edit_whiteboard', 'delete') |
34 | + 'edit', 'reviewer', 'edit_whiteboard', 'webhooks', 'delete') |
35 | |
36 | def branch_is_import(self): |
37 | return self.context.branch_type == BranchType.IMPORTED |
38 | @@ -267,6 +269,13 @@ |
39 | text = 'Set branch reviewer' |
40 | return Link('+reviewer', text, icon='edit') |
41 | |
42 | + @enabled_with_permission('launchpad.Edit') |
43 | + def webhooks(self): |
44 | + text = 'Manage webhooks' |
45 | + return Link( |
46 | + '+webhooks', text, icon='edit', |
47 | + enabled=bool(getFeatureFlag('webhooks.new.enabled'))) |
48 | + |
49 | |
50 | class BranchContextMenu(ContextMenu, HasRecipesMenuMixin, HasSnapsMenuMixin): |
51 | """Context menu for branches.""" |
52 | |
53 | === modified file 'lib/lp/code/configure.zcml' |
54 | --- lib/lp/code/configure.zcml 2015-09-11 15:11:34 +0000 |
55 | +++ lib/lp/code/configure.zcml 2015-09-29 15:54:57 +0000 |
56 | @@ -418,6 +418,9 @@ |
57 | for="lp.codehosting.scanner.events.ITipChanged" |
58 | handler="lp.codehosting.scanner.bzrsync.update_recipes"/> |
59 | <subscriber |
60 | + for="lp.codehosting.scanner.events.ITipChanged" |
61 | + handler="lp.codehosting.scanner.bzrsync.trigger_webhooks"/> |
62 | + <subscriber |
63 | for="lp.codehosting.scanner.events.IRevisionsRemoved" |
64 | handler="lp.codehosting.scanner.email.send_removed_revision_emails"/> |
65 | <subscriber |
66 | |
67 | === modified file 'lib/lp/code/interfaces/branch.py' |
68 | --- lib/lp/code/interfaces/branch.py 2015-09-29 00:32:52 +0000 |
69 | +++ lib/lp/code/interfaces/branch.py 2015-09-29 15:54:57 +0000 |
70 | @@ -105,6 +105,7 @@ |
71 | structured, |
72 | ) |
73 | from lp.services.webapp.interfaces import ITableBatchNavigator |
74 | +from lp.services.webhooks.interfaces import IWebhookTarget |
75 | |
76 | |
77 | DEFAULT_BRANCH_STATUS_IN_LISTING = ( |
78 | @@ -711,6 +712,10 @@ |
79 | Reference( |
80 | title=_("The associated CodeImport, if any."), schema=Interface)) |
81 | |
82 | + shortened_path = Attribute( |
83 | + "The shortest reasonable version of the path to this branch; as " |
84 | + "bzr_identity but without the 'lp:' prefix.") |
85 | + |
86 | bzr_identity = exported( |
87 | Text( |
88 | title=_('Bazaar Identity'), |
89 | @@ -780,7 +785,7 @@ |
90 | :return: A list of suite source packages ordered by pocket. |
91 | """ |
92 | |
93 | - def branchLinks(): |
94 | + def getBranchLinks(): |
95 | """Return a sorted list of ICanHasLinkedBranch objects. |
96 | |
97 | There is one result for each related linked object that the branch is |
98 | @@ -792,7 +797,7 @@ |
99 | more important links are sorted first. |
100 | """ |
101 | |
102 | - def branchIdentities(): |
103 | + def getBranchIdentities(): |
104 | """A list of aliases for a branch. |
105 | |
106 | Returns a list of tuples of bzr identity and context object. There is |
107 | @@ -805,10 +810,10 @@ |
108 | |
109 | For example, a branch linked to the development focus of the 'fooix' |
110 | project is accessible using: |
111 | - lp:fooix - the linked object is the product fooix |
112 | - lp:fooix/trunk - the linked object is the trunk series of fooix |
113 | - lp:~owner/fooix/name - the unique name of the branch where the |
114 | - linked object is the branch itself. |
115 | + fooix - the linked object is the product fooix |
116 | + fooix/trunk - the linked object is the trunk series of fooix |
117 | + ~owner/fooix/name - the unique name of the branch where the linked |
118 | + object is the branch itself. |
119 | """ |
120 | |
121 | # subscription-related methods |
122 | @@ -1129,7 +1134,7 @@ |
123 | vocabulary=ControlFormat)) |
124 | |
125 | |
126 | -class IBranchEdit(Interface): |
127 | +class IBranchEdit(IWebhookTarget): |
128 | """IBranch attributes that require launchpad.Edit permission.""" |
129 | |
130 | @call_with(user=REQUEST_USER) |
131 | @@ -1313,8 +1318,7 @@ |
132 | Return None if no match was found. |
133 | """ |
134 | |
135 | - @operation_parameters( |
136 | - url=TextLine(title=_('Branch URL'), required=True)) |
137 | + @operation_parameters(url=TextLine(title=_('Branch URL'), required=True)) |
138 | @operation_returns_entry(IBranch) |
139 | @export_read_operation() |
140 | @operation_for_version('beta') |
141 | @@ -1357,6 +1361,29 @@ |
142 | associated branch, the URL will map to `None`. |
143 | """ |
144 | |
145 | + @operation_parameters(path=TextLine(title=_('Branch path'), required=True)) |
146 | + @operation_returns_entry(IBranch) |
147 | + @export_read_operation() |
148 | + @operation_for_version('devel') |
149 | + def getByPath(path): |
150 | + """Find a branch by its path. |
151 | + |
152 | + The path is the same as its lp: URL, but without the leading lp:, so |
153 | + it may be in any of these forms:: |
154 | + |
155 | + Unique names: |
156 | + ~OWNER/PROJECT/NAME |
157 | + ~OWNER/DISTRO/SERIES/SOURCE/NAME |
158 | + ~OWNER/+junk/NAME |
159 | + Aliases linked to other objects: |
160 | + PROJECT |
161 | + PROJECT/SERIES |
162 | + DISTRO/SOURCE |
163 | + DISTRO/SUITE/SOURCE |
164 | + |
165 | + Return None if no match was found. |
166 | + """ |
167 | + |
168 | @collection_default_content() |
169 | def getBranches(limit=50, eager_load=True): |
170 | """Return a collection of branches. |
171 | @@ -1481,26 +1508,29 @@ |
172 | """ |
173 | |
174 | @property |
175 | + def shortened_path(self): |
176 | + """See `IBranch`.""" |
177 | + return self.getBranchIdentities()[0][0] |
178 | + |
179 | + @property |
180 | def bzr_identity(self): |
181 | """See `IBranch`.""" |
182 | - identity, context = self.branchIdentities()[0] |
183 | - return identity |
184 | + return config.codehosting.bzr_lp_prefix + self.shortened_path |
185 | |
186 | identity = bzr_identity |
187 | |
188 | - def branchIdentities(self): |
189 | + def getBranchIdentities(self): |
190 | """See `IBranch`.""" |
191 | - lp_prefix = config.codehosting.bzr_lp_prefix |
192 | - if not self.target.supports_short_identites: |
193 | + if not self.target.supports_short_identities: |
194 | identities = [] |
195 | else: |
196 | identities = [ |
197 | - (lp_prefix + link.bzr_path, link.context) |
198 | - for link in self.branchLinks()] |
199 | - identities.append((lp_prefix + self.unique_name, self)) |
200 | + (link.bzr_path, link.context) |
201 | + for link in self.getBranchLinks()] |
202 | + identities.append((self.unique_name, self)) |
203 | return identities |
204 | |
205 | - def branchLinks(self): |
206 | + def getBranchLinks(self): |
207 | """See `IBranch`.""" |
208 | links = [] |
209 | for suite_sp in self.associatedSuiteSourcePackages(): |
210 | |
211 | === modified file 'lib/lp/code/interfaces/branchlookup.py' |
212 | --- lib/lp/code/interfaces/branchlookup.py 2013-01-07 02:40:55 +0000 |
213 | +++ lib/lp/code/interfaces/branchlookup.py 2015-09-29 15:54:57 +0000 |
214 | @@ -158,6 +158,14 @@ |
215 | paths are not handled for shortcut paths. |
216 | """ |
217 | |
218 | + def getByPath(path): |
219 | + """Find the branch associated with a path. |
220 | + |
221 | + As with `getByLPPath`, but returns None instead of raising any of |
222 | + the documented exceptions, and returns only the `IBranch` on success |
223 | + and not any extra path. |
224 | + """ |
225 | + |
226 | |
227 | def path_lookups(path): |
228 | if path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'): |
229 | |
230 | === modified file 'lib/lp/code/interfaces/branchtarget.py' |
231 | --- lib/lp/code/interfaces/branchtarget.py 2015-04-21 14:32:31 +0000 |
232 | +++ lib/lp/code/interfaces/branchtarget.py 2015-09-29 15:54:57 +0000 |
233 | @@ -92,7 +92,7 @@ |
234 | supports_merge_proposals = Attribute( |
235 | "Does this target support merge proposals at all?") |
236 | |
237 | - supports_short_identites = Attribute( |
238 | + supports_short_identities = Attribute( |
239 | "Does this target support shortened bazaar identities?") |
240 | |
241 | supports_code_imports = Attribute( |
242 | |
243 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
244 | --- lib/lp/code/interfaces/gitrepository.py 2015-09-21 10:08:08 +0000 |
245 | +++ lib/lp/code/interfaces/gitrepository.py 2015-09-29 15:54:57 +0000 |
246 | @@ -688,7 +688,8 @@ |
247 | def getByPath(user, path): |
248 | """Find a repository by its path. |
249 | |
250 | - Any of these forms may be used, with or without a leading slash: |
251 | + Any of these forms may be used:: |
252 | + |
253 | Unique names: |
254 | ~OWNER/PROJECT/+git/NAME |
255 | ~OWNER/DISTRO/+source/SOURCE/+git/NAME |
256 | |
257 | === modified file 'lib/lp/code/model/branch.py' |
258 | --- lib/lp/code/model/branch.py 2015-09-29 00:32:52 +0000 |
259 | +++ lib/lp/code/model/branch.py 2015-09-29 15:54:57 +0000 |
260 | @@ -180,10 +180,12 @@ |
261 | from lp.services.webapp import urlappend |
262 | from lp.services.webapp.authorization import check_permission |
263 | from lp.services.webapp.interfaces import ILaunchBag |
264 | +from lp.services.webhooks.interfaces import IWebhookSet |
265 | +from lp.services.webhooks.model import WebhookTargetMixin |
266 | |
267 | |
268 | @implementer(IBranch, IPrivacy, IInformationType) |
269 | -class Branch(SQLBase, BzrIdentityMixin): |
270 | +class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin): |
271 | """A sequence of ordered revisions in Bazaar.""" |
272 | _table = 'Branch' |
273 | |
274 | @@ -1336,6 +1338,7 @@ |
275 | |
276 | self._deleteBranchSubscriptions() |
277 | self._deleteJobs() |
278 | + getUtility(IWebhookSet).delete(self.webhooks) |
279 | |
280 | # Now destroy the branch. |
281 | branch_id = self.id |
282 | @@ -1610,6 +1613,10 @@ |
283 | """See `IBranchSet`.""" |
284 | return getUtility(IBranchLookup).getByUrls(urls) |
285 | |
286 | + def getByPath(self, path): |
287 | + """See `IBranchSet`.""" |
288 | + return getUtility(IBranchLookup).getByPath(path) |
289 | + |
290 | def getBranches(self, limit=50, eager_load=True): |
291 | """See `IBranchSet`.""" |
292 | anon_branches = getUtility(IAllBranches).visibleByUser(None) |
293 | |
294 | === modified file 'lib/lp/code/model/branchlookup.py' |
295 | --- lib/lp/code/model/branchlookup.py 2015-07-10 05:57:13 +0000 |
296 | +++ lib/lp/code/model/branchlookup.py 2015-09-29 15:54:57 +0000 |
297 | @@ -248,14 +248,7 @@ |
298 | if uri.scheme == 'lp': |
299 | if not self._uriHostAllowed(uri): |
300 | return None |
301 | - try: |
302 | - return self.getByLPPath(uri.path.lstrip('/'))[0] |
303 | - except ( |
304 | - CannotHaveLinkedBranch, InvalidNamespace, InvalidProductName, |
305 | - NoSuchBranch, NoSuchPerson, NoSuchProduct, |
306 | - NoSuchProductSeries, NoSuchDistroSeries, |
307 | - NoSuchSourcePackageName, NoLinkedBranch): |
308 | - return None |
309 | + return self.getByPath(uri.path.lstrip('/')) |
310 | |
311 | return Branch.selectOneBy(url=url) |
312 | |
313 | @@ -386,6 +379,16 @@ |
314 | suffix = path[len(bzr_path) + 1:] |
315 | return branch, suffix |
316 | |
317 | + def getByPath(self, path): |
318 | + """See `IBranchLookup`.""" |
319 | + try: |
320 | + return self.getByLPPath(path)[0] |
321 | + except ( |
322 | + CannotHaveLinkedBranch, InvalidNamespace, InvalidProductName, |
323 | + NoSuchBranch, NoSuchPerson, NoSuchProduct, NoSuchProductSeries, |
324 | + NoSuchDistroSeries, NoSuchSourcePackageName, NoLinkedBranch): |
325 | + return None |
326 | + |
327 | def _getLinkedBranchAndPath(self, provided): |
328 | """Get the linked branch for 'provided', and the bzr_path. |
329 | |
330 | |
331 | === modified file 'lib/lp/code/model/branchtarget.py' |
332 | --- lib/lp/code/model/branchtarget.py 2015-07-08 16:05:11 +0000 |
333 | +++ lib/lp/code/model/branchtarget.py 2015-09-29 15:54:57 +0000 |
334 | @@ -113,7 +113,7 @@ |
335 | return True |
336 | |
337 | @property |
338 | - def supports_short_identites(self): |
339 | + def supports_short_identities(self): |
340 | """See `IBranchTarget`.""" |
341 | return True |
342 | |
343 | @@ -229,7 +229,7 @@ |
344 | return False |
345 | |
346 | @property |
347 | - def supports_short_identites(self): |
348 | + def supports_short_identities(self): |
349 | """See `IBranchTarget`.""" |
350 | return False |
351 | |
352 | @@ -314,7 +314,7 @@ |
353 | return True |
354 | |
355 | @property |
356 | - def supports_short_identites(self): |
357 | + def supports_short_identities(self): |
358 | """See `IBranchTarget`.""" |
359 | return True |
360 | |
361 | |
362 | === modified file 'lib/lp/code/model/tests/test_branch.py' |
363 | --- lib/lp/code/model/tests/test_branch.py 2015-08-06 12:03:36 +0000 |
364 | +++ lib/lp/code/model/tests/test_branch.py 2015-09-29 15:54:57 +0000 |
365 | @@ -910,202 +910,8 @@ |
366 | self.assertFalse(branch.upgrade_pending) |
367 | |
368 | |
369 | -class TestBranchLinksAndIdentites(TestCaseWithFactory): |
370 | - """Test IBranch.branchLinks and IBranch.branchIdentities.""" |
371 | - |
372 | - layer = DatabaseFunctionalLayer |
373 | - |
374 | - def test_default_identities(self): |
375 | - # If there are no links, the only branch identity is the unique name. |
376 | - branch = self.factory.makeAnyBranch() |
377 | - self.assertEqual( |
378 | - [('lp://dev/' + branch.unique_name, branch)], |
379 | - branch.branchIdentities()) |
380 | - |
381 | - def test_linked_to_product(self): |
382 | - # If a branch is linked to the product, it is also by definition |
383 | - # linked to the development focus of the product. |
384 | - fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix')) |
385 | - fooix.development_focus.name = 'devel' |
386 | - eric = self.factory.makePerson(name='eric') |
387 | - branch = self.factory.makeProductBranch( |
388 | - product=fooix, owner=eric, name='trunk') |
389 | - linked_branch = ICanHasLinkedBranch(fooix) |
390 | - linked_branch.setBranch(branch) |
391 | - self.assertEqual( |
392 | - [linked_branch, ICanHasLinkedBranch(fooix.development_focus)], |
393 | - branch.branchLinks()) |
394 | - self.assertEqual( |
395 | - [('lp://dev/fooix', fooix), |
396 | - ('lp://dev/fooix/devel', fooix.development_focus), |
397 | - ('lp://dev/~eric/fooix/trunk', branch)], |
398 | - branch.branchIdentities()) |
399 | - |
400 | - def test_linked_to_product_series(self): |
401 | - # If a branch is linked to a non-development series of a product and |
402 | - # not linked to the product itself, then only the product series is |
403 | - # returned in the links. |
404 | - fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix')) |
405 | - future = self.factory.makeProductSeries(product=fooix, name='future') |
406 | - eric = self.factory.makePerson(name='eric') |
407 | - branch = self.factory.makeProductBranch( |
408 | - product=fooix, owner=eric, name='trunk') |
409 | - linked_branch = ICanHasLinkedBranch(future) |
410 | - login_person(fooix.owner) |
411 | - linked_branch.setBranch(branch) |
412 | - self.assertEqual( |
413 | - [linked_branch], |
414 | - branch.branchLinks()) |
415 | - self.assertEqual( |
416 | - [('lp://dev/fooix/future', future), |
417 | - ('lp://dev/~eric/fooix/trunk', branch)], |
418 | - branch.branchIdentities()) |
419 | - |
420 | - def test_linked_to_package(self): |
421 | - # If a branch is linked to a suite source package where the |
422 | - # distroseries is the current series for the distribution, there is a |
423 | - # link for both the distribution source package and the suite source |
424 | - # package. |
425 | - mint = self.factory.makeDistribution(name='mint') |
426 | - dev = self.factory.makeDistroSeries( |
427 | - distribution=mint, version='1.0', name='dev') |
428 | - eric = self.factory.makePerson(name='eric') |
429 | - branch = self.factory.makePackageBranch( |
430 | - distroseries=dev, sourcepackagename='choc', name='tip', |
431 | - owner=eric) |
432 | - dsp = self.factory.makeDistributionSourcePackage('choc', mint) |
433 | - distro_link = ICanHasLinkedBranch(dsp) |
434 | - development_package = dsp.development_version |
435 | - suite_sourcepackage = development_package.getSuiteSourcePackage( |
436 | - PackagePublishingPocket.RELEASE) |
437 | - suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage) |
438 | - |
439 | - registrant = suite_sourcepackage.distribution.owner |
440 | - run_with_login( |
441 | - registrant, |
442 | - suite_sp_link.setBranch, branch, registrant) |
443 | - |
444 | - self.assertEqual( |
445 | - [distro_link, suite_sp_link], |
446 | - branch.branchLinks()) |
447 | - self.assertEqual( |
448 | - [('lp://dev/mint/choc', dsp), |
449 | - ('lp://dev/mint/dev/choc', suite_sourcepackage), |
450 | - ('lp://dev/~eric/mint/dev/choc/tip', branch)], |
451 | - branch.branchIdentities()) |
452 | - |
453 | - def test_linked_to_package_not_release_pocket(self): |
454 | - # If a branch is linked to a suite source package where the |
455 | - # distroseries is the current series for the distribution, but the |
456 | - # pocket is not the RELEASE pocket, then there is only the link for |
457 | - # the suite source package. |
458 | - mint = self.factory.makeDistribution(name='mint') |
459 | - dev = self.factory.makeDistroSeries( |
460 | - distribution=mint, version='1.0', name='dev') |
461 | - eric = self.factory.makePerson(name='eric') |
462 | - branch = self.factory.makePackageBranch( |
463 | - distroseries=dev, sourcepackagename='choc', name='tip', |
464 | - owner=eric) |
465 | - dsp = self.factory.makeDistributionSourcePackage('choc', mint) |
466 | - development_package = dsp.development_version |
467 | - suite_sourcepackage = development_package.getSuiteSourcePackage( |
468 | - PackagePublishingPocket.BACKPORTS) |
469 | - suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage) |
470 | - |
471 | - registrant = suite_sourcepackage.distribution.owner |
472 | - run_with_login( |
473 | - registrant, |
474 | - suite_sp_link.setBranch, branch, registrant) |
475 | - |
476 | - self.assertEqual( |
477 | - [suite_sp_link], |
478 | - branch.branchLinks()) |
479 | - self.assertEqual( |
480 | - [('lp://dev/mint/dev-backports/choc', suite_sourcepackage), |
481 | - ('lp://dev/~eric/mint/dev/choc/tip', branch)], |
482 | - branch.branchIdentities()) |
483 | - |
484 | - def test_linked_to_package_not_current_series(self): |
485 | - # If the branch is linked to a suite source package where the distro |
486 | - # series is not the current series, only the suite source package is |
487 | - # returned in the links. |
488 | - mint = self.factory.makeDistribution(name='mint') |
489 | - self.factory.makeDistroSeries( |
490 | - distribution=mint, version='1.0', name='dev') |
491 | - supported = self.factory.makeDistroSeries( |
492 | - distribution=mint, version='0.9', name='supported') |
493 | - eric = self.factory.makePerson(name='eric') |
494 | - branch = self.factory.makePackageBranch( |
495 | - distroseries=supported, sourcepackagename='choc', name='tip', |
496 | - owner=eric) |
497 | - suite_sp = self.factory.makeSuiteSourcePackage( |
498 | - distroseries=supported, sourcepackagename='choc', |
499 | - pocket=PackagePublishingPocket.RELEASE) |
500 | - suite_sp_link = ICanHasLinkedBranch(suite_sp) |
501 | - |
502 | - registrant = suite_sp.distribution.owner |
503 | - run_with_login( |
504 | - registrant, |
505 | - suite_sp_link.setBranch, branch, registrant) |
506 | - |
507 | - self.assertEqual( |
508 | - [suite_sp_link], |
509 | - branch.branchLinks()) |
510 | - self.assertEqual( |
511 | - [('lp://dev/mint/supported/choc', suite_sp), |
512 | - ('lp://dev/~eric/mint/supported/choc/tip', branch)], |
513 | - branch.branchIdentities()) |
514 | - |
515 | - def test_linked_across_project_to_package(self): |
516 | - # If a product branch is linked to a suite source package, the links |
517 | - # are the same as if it was a source package branch. |
518 | - mint = self.factory.makeDistribution(name='mint') |
519 | - self.factory.makeDistroSeries( |
520 | - distribution=mint, version='1.0', name='dev') |
521 | - eric = self.factory.makePerson(name='eric') |
522 | - fooix = self.factory.makeProduct(name='fooix') |
523 | - branch = self.factory.makeProductBranch( |
524 | - product=fooix, owner=eric, name='trunk') |
525 | - dsp = self.factory.makeDistributionSourcePackage('choc', mint) |
526 | - distro_link = ICanHasLinkedBranch(dsp) |
527 | - development_package = dsp.development_version |
528 | - suite_sourcepackage = development_package.getSuiteSourcePackage( |
529 | - PackagePublishingPocket.RELEASE) |
530 | - suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage) |
531 | - |
532 | - registrant = suite_sourcepackage.distribution.owner |
533 | - run_with_login( |
534 | - registrant, |
535 | - suite_sp_link.setBranch, branch, registrant) |
536 | - |
537 | - self.assertEqual( |
538 | - [distro_link, suite_sp_link], |
539 | - branch.branchLinks()) |
540 | - self.assertEqual( |
541 | - [('lp://dev/mint/choc', dsp), |
542 | - ('lp://dev/mint/dev/choc', suite_sourcepackage), |
543 | - ('lp://dev/~eric/fooix/trunk', branch)], |
544 | - branch.branchIdentities()) |
545 | - |
546 | - def test_junk_branch_links(self): |
547 | - # If a junk branch has links, those links are returned in the |
548 | - # branchLinks, but the branchIdentities just has the branch unique |
549 | - # name. |
550 | - eric = self.factory.makePerson(name='eric') |
551 | - branch = self.factory.makePersonalBranch(owner=eric, name='foo') |
552 | - fooix = removeSecurityProxy(self.factory.makeProduct()) |
553 | - linked_branch = ICanHasLinkedBranch(fooix) |
554 | - linked_branch.setBranch(branch) |
555 | - self.assertEqual( |
556 | - [linked_branch, ICanHasLinkedBranch(fooix.development_focus)], |
557 | - branch.branchLinks()) |
558 | - self.assertEqual( |
559 | - [('lp://dev/~eric/+junk/foo', branch)], |
560 | - branch.branchIdentities()) |
561 | - |
562 | - |
563 | -class TestBzrIdentity(TestCaseWithFactory): |
564 | - """Test IBranch.bzr_identity.""" |
565 | +class TestBzrIdentityMixin(TestCaseWithFactory): |
566 | + """Test the defaults and identities provided by BzrIdentityMixin.""" |
567 | |
568 | layer = DatabaseFunctionalLayer |
569 | |
570 | @@ -1115,16 +921,17 @@ |
571 | Actually, it'll be lp://dev/<identity_path>. |
572 | """ |
573 | self.assertEqual( |
574 | - 'lp://dev/%s' % identity_path, branch.bzr_identity, |
575 | - "bzr identity") |
576 | + identity_path, branch.shortened_path, "shortened path") |
577 | + self.assertEqual( |
578 | + 'lp://dev/%s' % identity_path, branch.bzr_identity, "bzr identity") |
579 | |
580 | - def test_default_identity(self): |
581 | + def test_bzr_identity_default(self): |
582 | # By default, the bzr identity is an lp URL with the branch's unique |
583 | # name. |
584 | branch = self.factory.makeAnyBranch() |
585 | self.assertBzrIdentity(branch, branch.unique_name) |
586 | |
587 | - def test_linked_to_product(self): |
588 | + def test_bzr_identity_linked_to_product(self): |
589 | # If a branch is the development focus branch for a product, then it's |
590 | # bzr identity is lp:product. |
591 | branch = self.factory.makeProductBranch() |
592 | @@ -1133,7 +940,7 @@ |
593 | linked_branch.setBranch(branch) |
594 | self.assertBzrIdentity(branch, linked_branch.bzr_path) |
595 | |
596 | - def test_linked_to_product_series(self): |
597 | + def test_bzr_identity_linked_to_product_series(self): |
598 | # If a branch is the development focus branch for a product series, |
599 | # then it's bzr identity is lp:product/series. |
600 | branch = self.factory.makeProductBranch() |
601 | @@ -1144,7 +951,7 @@ |
602 | linked_branch.setBranch(branch) |
603 | self.assertBzrIdentity(branch, linked_branch.bzr_path) |
604 | |
605 | - def test_private_linked_to_product(self): |
606 | + def test_bzr_identity_private_linked_to_product(self): |
607 | # Private branches also have a short lp:url. |
608 | branch = self.factory.makeProductBranch( |
609 | information_type=InformationType.USERDATA) |
610 | @@ -1153,7 +960,7 @@ |
611 | ICanHasLinkedBranch(product).setBranch(branch) |
612 | self.assertBzrIdentity(branch, product.name) |
613 | |
614 | - def test_linked_to_series_and_dev_focus(self): |
615 | + def test_bzr_identity_linked_to_series_and_dev_focus(self): |
616 | # If a branch is the development focus branch for a product and the |
617 | # branch for a series, the bzr identity will be the storter of the two |
618 | # URLs. |
619 | @@ -1167,7 +974,7 @@ |
620 | series_link.setBranch(branch) |
621 | self.assertBzrIdentity(branch, product_link.bzr_path) |
622 | |
623 | - def test_junk_branch_always_unique_name(self): |
624 | + def test_bzr_identity_junk_branch_always_unique_name(self): |
625 | # For junk branches, the bzr identity is always based on the unique |
626 | # name of the branch, even if it's linked to a product, product series |
627 | # or whatever. |
628 | @@ -1176,7 +983,7 @@ |
629 | ICanHasLinkedBranch(product).setBranch(branch) |
630 | self.assertBzrIdentity(branch, branch.unique_name) |
631 | |
632 | - def test_linked_to_package(self): |
633 | + def test_bzr_identity_linked_to_package(self): |
634 | # If a branch is linked to a pocket of a package, then the |
635 | # bzr identity is the path to that package. |
636 | branch = self.factory.makePackageBranch() |
637 | @@ -1191,7 +998,7 @@ |
638 | login(ANONYMOUS) |
639 | self.assertBzrIdentity(branch, linked_branch.bzr_path) |
640 | |
641 | - def test_linked_to_dev_package(self): |
642 | + def test_bzr_identity_linked_to_dev_package(self): |
643 | # If a branch is linked to the development focus version of a package |
644 | # then the bzr identity is distro/package. |
645 | sourcepackage = self.factory.makeSourcePackage() |
646 | @@ -1205,6 +1012,192 @@ |
647 | linked_branch.setBranch, branch, registrant) |
648 | self.assertBzrIdentity(branch, linked_branch.bzr_path) |
649 | |
650 | + def test_identities_no_links(self): |
651 | + # If there are no links, the only branch identity is the unique name. |
652 | + branch = self.factory.makeAnyBranch() |
653 | + self.assertEqual( |
654 | + [(branch.unique_name, branch)], branch.getBranchIdentities()) |
655 | + |
656 | + def test_linked_to_product(self): |
657 | + # If a branch is linked to the product, it is also by definition |
658 | + # linked to the development focus of the product. |
659 | + fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix')) |
660 | + fooix.development_focus.name = 'devel' |
661 | + eric = self.factory.makePerson(name='eric') |
662 | + branch = self.factory.makeProductBranch( |
663 | + product=fooix, owner=eric, name='trunk') |
664 | + linked_branch = ICanHasLinkedBranch(fooix) |
665 | + linked_branch.setBranch(branch) |
666 | + self.assertEqual( |
667 | + [linked_branch, ICanHasLinkedBranch(fooix.development_focus)], |
668 | + branch.getBranchLinks()) |
669 | + self.assertEqual( |
670 | + [('fooix', fooix), |
671 | + ('fooix/devel', fooix.development_focus), |
672 | + ('~eric/fooix/trunk', branch)], |
673 | + branch.getBranchIdentities()) |
674 | + |
675 | + def test_linked_to_product_series(self): |
676 | + # If a branch is linked to a non-development series of a product and |
677 | + # not linked to the product itself, then only the product series is |
678 | + # returned in the links. |
679 | + fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix')) |
680 | + future = self.factory.makeProductSeries(product=fooix, name='future') |
681 | + eric = self.factory.makePerson(name='eric') |
682 | + branch = self.factory.makeProductBranch( |
683 | + product=fooix, owner=eric, name='trunk') |
684 | + linked_branch = ICanHasLinkedBranch(future) |
685 | + login_person(fooix.owner) |
686 | + linked_branch.setBranch(branch) |
687 | + self.assertEqual( |
688 | + [linked_branch], |
689 | + branch.getBranchLinks()) |
690 | + self.assertEqual( |
691 | + [('fooix/future', future), |
692 | + ('~eric/fooix/trunk', branch)], |
693 | + branch.getBranchIdentities()) |
694 | + |
695 | + def test_linked_to_package(self): |
696 | + # If a branch is linked to a suite source package where the |
697 | + # distroseries is the current series for the distribution, there is a |
698 | + # link for both the distribution source package and the suite source |
699 | + # package. |
700 | + mint = self.factory.makeDistribution(name='mint') |
701 | + dev = self.factory.makeDistroSeries( |
702 | + distribution=mint, version='1.0', name='dev') |
703 | + eric = self.factory.makePerson(name='eric') |
704 | + branch = self.factory.makePackageBranch( |
705 | + distroseries=dev, sourcepackagename='choc', name='tip', |
706 | + owner=eric) |
707 | + dsp = self.factory.makeDistributionSourcePackage('choc', mint) |
708 | + distro_link = ICanHasLinkedBranch(dsp) |
709 | + development_package = dsp.development_version |
710 | + suite_sourcepackage = development_package.getSuiteSourcePackage( |
711 | + PackagePublishingPocket.RELEASE) |
712 | + suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage) |
713 | + |
714 | + registrant = suite_sourcepackage.distribution.owner |
715 | + run_with_login( |
716 | + registrant, |
717 | + suite_sp_link.setBranch, branch, registrant) |
718 | + |
719 | + self.assertEqual( |
720 | + [distro_link, suite_sp_link], |
721 | + branch.getBranchLinks()) |
722 | + self.assertEqual( |
723 | + [('mint/choc', dsp), |
724 | + ('mint/dev/choc', suite_sourcepackage), |
725 | + ('~eric/mint/dev/choc/tip', branch)], |
726 | + branch.getBranchIdentities()) |
727 | + |
728 | + def test_linked_to_package_not_release_pocket(self): |
729 | + # If a branch is linked to a suite source package where the |
730 | + # distroseries is the current series for the distribution, but the |
731 | + # pocket is not the RELEASE pocket, then there is only the link for |
732 | + # the suite source package. |
733 | + mint = self.factory.makeDistribution(name='mint') |
734 | + dev = self.factory.makeDistroSeries( |
735 | + distribution=mint, version='1.0', name='dev') |
736 | + eric = self.factory.makePerson(name='eric') |
737 | + branch = self.factory.makePackageBranch( |
738 | + distroseries=dev, sourcepackagename='choc', name='tip', |
739 | + owner=eric) |
740 | + dsp = self.factory.makeDistributionSourcePackage('choc', mint) |
741 | + development_package = dsp.development_version |
742 | + suite_sourcepackage = development_package.getSuiteSourcePackage( |
743 | + PackagePublishingPocket.BACKPORTS) |
744 | + suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage) |
745 | + |
746 | + registrant = suite_sourcepackage.distribution.owner |
747 | + run_with_login( |
748 | + registrant, |
749 | + suite_sp_link.setBranch, branch, registrant) |
750 | + |
751 | + self.assertEqual( |
752 | + [suite_sp_link], |
753 | + branch.getBranchLinks()) |
754 | + self.assertEqual( |
755 | + [('mint/dev-backports/choc', suite_sourcepackage), |
756 | + ('~eric/mint/dev/choc/tip', branch)], |
757 | + branch.getBranchIdentities()) |
758 | + |
759 | + def test_linked_to_package_not_current_series(self): |
760 | + # If the branch is linked to a suite source package where the distro |
761 | + # series is not the current series, only the suite source package is |
762 | + # returned in the links. |
763 | + mint = self.factory.makeDistribution(name='mint') |
764 | + self.factory.makeDistroSeries( |
765 | + distribution=mint, version='1.0', name='dev') |
766 | + supported = self.factory.makeDistroSeries( |
767 | + distribution=mint, version='0.9', name='supported') |
768 | + eric = self.factory.makePerson(name='eric') |
769 | + branch = self.factory.makePackageBranch( |
770 | + distroseries=supported, sourcepackagename='choc', name='tip', |
771 | + owner=eric) |
772 | + suite_sp = self.factory.makeSuiteSourcePackage( |
773 | + distroseries=supported, sourcepackagename='choc', |
774 | + pocket=PackagePublishingPocket.RELEASE) |
775 | + suite_sp_link = ICanHasLinkedBranch(suite_sp) |
776 | + |
777 | + registrant = suite_sp.distribution.owner |
778 | + run_with_login( |
779 | + registrant, |
780 | + suite_sp_link.setBranch, branch, registrant) |
781 | + |
782 | + self.assertEqual( |
783 | + [suite_sp_link], |
784 | + branch.getBranchLinks()) |
785 | + self.assertEqual( |
786 | + [('mint/supported/choc', suite_sp), |
787 | + ('~eric/mint/supported/choc/tip', branch)], |
788 | + branch.getBranchIdentities()) |
789 | + |
790 | + def test_linked_across_project_to_package(self): |
791 | + # If a product branch is linked to a suite source package, the links |
792 | + # are the same as if it was a source package branch. |
793 | + mint = self.factory.makeDistribution(name='mint') |
794 | + self.factory.makeDistroSeries( |
795 | + distribution=mint, version='1.0', name='dev') |
796 | + eric = self.factory.makePerson(name='eric') |
797 | + fooix = self.factory.makeProduct(name='fooix') |
798 | + branch = self.factory.makeProductBranch( |
799 | + product=fooix, owner=eric, name='trunk') |
800 | + dsp = self.factory.makeDistributionSourcePackage('choc', mint) |
801 | + distro_link = ICanHasLinkedBranch(dsp) |
802 | + development_package = dsp.development_version |
803 | + suite_sourcepackage = development_package.getSuiteSourcePackage( |
804 | + PackagePublishingPocket.RELEASE) |
805 | + suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage) |
806 | + |
807 | + registrant = suite_sourcepackage.distribution.owner |
808 | + run_with_login( |
809 | + registrant, |
810 | + suite_sp_link.setBranch, branch, registrant) |
811 | + |
812 | + self.assertEqual( |
813 | + [distro_link, suite_sp_link], |
814 | + branch.getBranchLinks()) |
815 | + self.assertEqual( |
816 | + [('mint/choc', dsp), |
817 | + ('mint/dev/choc', suite_sourcepackage), |
818 | + ('~eric/fooix/trunk', branch)], |
819 | + branch.getBranchIdentities()) |
820 | + |
821 | + def test_junk_branch_links(self): |
822 | + # If a junk branch has links, those links are returned by |
823 | + # getBranchLinks, but getBranchIdentities just returns the branch |
824 | + # unique name. |
825 | + eric = self.factory.makePerson(name='eric') |
826 | + branch = self.factory.makePersonalBranch(owner=eric, name='foo') |
827 | + fooix = removeSecurityProxy(self.factory.makeProduct()) |
828 | + linked_branch = ICanHasLinkedBranch(fooix) |
829 | + linked_branch.setBranch(branch) |
830 | + self.assertEqual( |
831 | + [linked_branch, ICanHasLinkedBranch(fooix.development_focus)], |
832 | + branch.getBranchLinks()) |
833 | + self.assertEqual( |
834 | + [('~eric/+junk/foo', branch)], branch.getBranchIdentities()) |
835 | + |
836 | |
837 | class TestBranchDeletion(TestCaseWithFactory): |
838 | """Test the different cases that makes a branch deletable or not.""" |
839 | @@ -1442,6 +1435,13 @@ |
840 | ) |
841 | self.branch.destroySelf(break_references=True) |
842 | |
843 | + def test_related_webhooks_deleted(self): |
844 | + webhook = self.factory.makeWebhook(target=self.branch) |
845 | + webhook.ping() |
846 | + self.branch.destroySelf() |
847 | + transaction.commit() |
848 | + self.assertRaises(LostObjectError, getattr, webhook, 'target') |
849 | + |
850 | |
851 | class TestBranchDeletionConsequences(TestCase): |
852 | """Test determination and application of branch deletion consequences.""" |
853 | |
854 | === modified file 'lib/lp/code/model/tests/test_branchlookup.py' |
855 | --- lib/lp/code/model/tests/test_branchlookup.py 2012-11-12 13:38:27 +0000 |
856 | +++ lib/lp/code/model/tests/test_branchlookup.py 2015-09-29 15:54:57 +0000 |
857 | @@ -153,7 +153,7 @@ |
858 | self.assertEqual((branch, '/foo'), result) |
859 | |
860 | |
861 | -class TestGetByPath(TestCaseWithFactory): |
862 | +class TestGetByLPPath(TestCaseWithFactory): |
863 | """Test `IBranchLookup.getByLPPath`.""" |
864 | |
865 | layer = DatabaseFunctionalLayer |
866 | @@ -171,54 +171,51 @@ |
867 | self.factory.getUniqueString() |
868 | for i in range(arbitrary_num_segments)]) |
869 | |
870 | + def assertMissingPath(self, exctype, path): |
871 | + self.assertRaises(exctype, self.getByPath, path) |
872 | + |
873 | + def assertPath(self, expected_branch, expected_suffix, path): |
874 | + branch, suffix = self.getByPath(path) |
875 | + self.assertEqual(expected_branch, branch) |
876 | + self.assertEqual(expected_suffix, suffix) |
877 | + |
878 | def test_finds_exact_personal_branch(self): |
879 | branch = self.factory.makePersonalBranch() |
880 | - found_branch, suffix = self.getByPath(branch.unique_name) |
881 | - self.assertEqual(branch, found_branch) |
882 | - self.assertEqual('', suffix) |
883 | + self.assertPath(branch, '', branch.unique_name) |
884 | |
885 | def test_finds_suffixed_personal_branch(self): |
886 | branch = self.factory.makePersonalBranch() |
887 | suffix = self.makeRelativePath() |
888 | - found_branch, found_suffix = self.getByPath( |
889 | - branch.unique_name + '/' + suffix) |
890 | - self.assertEqual(branch, found_branch) |
891 | - self.assertEqual(suffix, found_suffix) |
892 | + self.assertPath(branch, suffix, branch.unique_name + '/' + suffix) |
893 | |
894 | def test_missing_personal_branch(self): |
895 | owner = self.factory.makePerson() |
896 | namespace = get_branch_namespace(owner) |
897 | branch_name = namespace.getBranchName(self.factory.getUniqueString()) |
898 | - self.assertRaises(NoSuchBranch, self.getByPath, branch_name) |
899 | + self.assertMissingPath(NoSuchBranch, branch_name) |
900 | |
901 | def test_missing_suffixed_personal_branch(self): |
902 | owner = self.factory.makePerson() |
903 | namespace = get_branch_namespace(owner) |
904 | branch_name = namespace.getBranchName(self.factory.getUniqueString()) |
905 | suffix = self.makeRelativePath() |
906 | - self.assertRaises( |
907 | - NoSuchBranch, self.getByPath, branch_name + '/' + suffix) |
908 | + self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix) |
909 | |
910 | def test_finds_exact_product_branch(self): |
911 | branch = self.factory.makeProductBranch() |
912 | - found_branch, suffix = self.getByPath(branch.unique_name) |
913 | - self.assertEqual(branch, found_branch) |
914 | - self.assertEqual('', suffix) |
915 | + self.assertPath(branch, '', branch.unique_name) |
916 | |
917 | def test_finds_suffixed_product_branch(self): |
918 | branch = self.factory.makeProductBranch() |
919 | suffix = self.makeRelativePath() |
920 | - found_branch, found_suffix = self.getByPath( |
921 | - branch.unique_name + '/' + suffix) |
922 | - self.assertEqual(branch, found_branch) |
923 | - self.assertEqual(suffix, found_suffix) |
924 | + self.assertPath(branch, suffix, branch.unique_name + '/' + suffix) |
925 | |
926 | def test_missing_product_branch(self): |
927 | owner = self.factory.makePerson() |
928 | product = self.factory.makeProduct() |
929 | namespace = get_branch_namespace(owner, product=product) |
930 | branch_name = namespace.getBranchName(self.factory.getUniqueString()) |
931 | - self.assertRaises(NoSuchBranch, self.getByPath, branch_name) |
932 | + self.assertMissingPath(NoSuchBranch, branch_name) |
933 | |
934 | def test_missing_suffixed_product_branch(self): |
935 | owner = self.factory.makePerson() |
936 | @@ -226,14 +223,11 @@ |
937 | namespace = get_branch_namespace(owner, product=product) |
938 | suffix = self.makeRelativePath() |
939 | branch_name = namespace.getBranchName(self.factory.getUniqueString()) |
940 | - self.assertRaises( |
941 | - NoSuchBranch, self.getByPath, branch_name + '/' + suffix) |
942 | + self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix) |
943 | |
944 | def test_finds_exact_package_branch(self): |
945 | branch = self.factory.makePackageBranch() |
946 | - found_branch, suffix = self.getByPath(branch.unique_name) |
947 | - self.assertEqual(branch, found_branch) |
948 | - self.assertEqual('', suffix) |
949 | + self.assertPath(branch, '', branch.unique_name) |
950 | |
951 | def test_missing_package_branch(self): |
952 | owner = self.factory.makePerson() |
953 | @@ -243,7 +237,7 @@ |
954 | owner, distroseries=distroseries, |
955 | sourcepackagename=sourcepackagename) |
956 | branch_name = namespace.getBranchName(self.factory.getUniqueString()) |
957 | - self.assertRaises(NoSuchBranch, self.getByPath, branch_name) |
958 | + self.assertMissingPath(NoSuchBranch, branch_name) |
959 | |
960 | def test_missing_suffixed_package_branch(self): |
961 | owner = self.factory.makePerson() |
962 | @@ -254,19 +248,30 @@ |
963 | sourcepackagename=sourcepackagename) |
964 | suffix = self.makeRelativePath() |
965 | branch_name = namespace.getBranchName(self.factory.getUniqueString()) |
966 | - self.assertRaises( |
967 | - NoSuchBranch, self.getByPath, branch_name + '/' + suffix) |
968 | + self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix) |
969 | |
970 | def test_too_short(self): |
971 | person = self.factory.makePerson() |
972 | - self.assertRaises( |
973 | - InvalidNamespace, self.getByPath, '~%s' % person.name) |
974 | + self.assertMissingPath(InvalidNamespace, '~%s' % person.name) |
975 | |
976 | def test_no_such_product(self): |
977 | person = self.factory.makePerson() |
978 | branch_name = '~%s/%s/%s' % ( |
979 | person.name, self.factory.getUniqueString(), 'branch-name') |
980 | - self.assertRaises(NoSuchProduct, self.getByPath, branch_name) |
981 | + self.assertMissingPath(NoSuchProduct, branch_name) |
982 | + |
983 | + |
984 | +class TestGetByPath(TestGetByLPPath): |
985 | + """Test `IBranchLookup.getByPath`.""" |
986 | + |
987 | + def getByPath(self, path): |
988 | + return self.branch_lookup.getByPath(path) |
989 | + |
990 | + def assertMissingPath(self, exctype, path): |
991 | + self.assertIsNone(self.getByPath(path)) |
992 | + |
993 | + def assertPath(self, expected_branch, expected_suffix, path): |
994 | + self.assertEqual(expected_branch, self.getByPath(path)) |
995 | |
996 | |
997 | class TestGetByUrl(TestCaseWithFactory): |
998 | |
999 | === modified file 'lib/lp/code/model/tests/test_branchset.py' |
1000 | --- lib/lp/code/model/tests/test_branchset.py 2012-09-18 18:36:09 +0000 |
1001 | +++ lib/lp/code/model/tests/test_branchset.py 2015-09-29 15:54:57 +0000 |
1002 | @@ -10,7 +10,9 @@ |
1003 | |
1004 | from lp.app.enums import InformationType |
1005 | from lp.code.interfaces.branch import IBranchSet |
1006 | +from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch |
1007 | from lp.code.model.branch import BranchSet |
1008 | +from lp.services.propertycache import clear_property_cache |
1009 | from lp.testing import ( |
1010 | login_person, |
1011 | logout, |
1012 | @@ -46,6 +48,16 @@ |
1013 | branches = BranchSet().getByUrls([url]) |
1014 | self.assertEqual({url: None}, branches) |
1015 | |
1016 | + def test_getByPath(self): |
1017 | + branch = self.factory.makeProductBranch() |
1018 | + self.assertEqual(branch, BranchSet().getByPath(branch.shortened_path)) |
1019 | + product = removeSecurityProxy(branch.product) |
1020 | + ICanHasLinkedBranch(product).setBranch(branch) |
1021 | + clear_property_cache(branch) |
1022 | + self.assertEqual(product.name, branch.shortened_path) |
1023 | + self.assertEqual(branch, BranchSet().getByPath(branch.shortened_path)) |
1024 | + self.assertIsNone(BranchSet().getByPath('nonexistent')) |
1025 | + |
1026 | def test_api_branches_query_count(self): |
1027 | webservice = LaunchpadWebServiceCaller() |
1028 | collector = QueryCollector() |
1029 | |
1030 | === modified file 'lib/lp/code/model/tests/test_branchtarget.py' |
1031 | --- lib/lp/code/model/tests/test_branchtarget.py 2014-11-28 22:07:05 +0000 |
1032 | +++ lib/lp/code/model/tests/test_branchtarget.py 2015-09-29 15:54:57 +0000 |
1033 | @@ -141,9 +141,9 @@ |
1034 | # Package branches do support merge proposals. |
1035 | self.assertTrue(self.target.supports_merge_proposals) |
1036 | |
1037 | - def test_supports_short_identites(self): |
1038 | - # Package branches do support short bzr identites. |
1039 | - self.assertTrue(self.target.supports_short_identites) |
1040 | + def test_supports_short_identities(self): |
1041 | + # Package branches do support short bzr identities. |
1042 | + self.assertTrue(self.target.supports_short_identities) |
1043 | |
1044 | def test_displayname(self): |
1045 | # The display name of a source package target is the display name of |
1046 | @@ -280,9 +280,9 @@ |
1047 | # Personal branches do not support merge proposals. |
1048 | self.assertFalse(self.target.supports_merge_proposals) |
1049 | |
1050 | - def test_supports_short_identites(self): |
1051 | - # Personal branches do not support short bzr identites. |
1052 | - self.assertFalse(self.target.supports_short_identites) |
1053 | + def test_supports_short_identities(self): |
1054 | + # Personal branches do not support short bzr identities. |
1055 | + self.assertFalse(self.target.supports_short_identities) |
1056 | |
1057 | def test_displayname(self): |
1058 | # The display name of a person branch target is ~$USER/+junk. |
1059 | @@ -405,9 +405,9 @@ |
1060 | # Product branches do support merge proposals. |
1061 | self.assertTrue(self.target.supports_merge_proposals) |
1062 | |
1063 | - def test_supports_short_identites(self): |
1064 | - # Product branches do support short bzr identites. |
1065 | - self.assertTrue(self.target.supports_short_identites) |
1066 | + def test_supports_short_identities(self): |
1067 | + # Product branches do support short bzr identities. |
1068 | + self.assertTrue(self.target.supports_short_identities) |
1069 | |
1070 | def test_displayname(self): |
1071 | # The display name of a product branch target is the display name of |
1072 | |
1073 | === modified file 'lib/lp/codehosting/scanner/bzrsync.py' |
1074 | --- lib/lp/codehosting/scanner/bzrsync.py 2015-02-25 12:48:27 +0000 |
1075 | +++ lib/lp/codehosting/scanner/bzrsync.py 2015-09-29 15:54:57 +0000 |
1076 | @@ -1,6 +1,6 @@ |
1077 | #!/usr/bin/python |
1078 | # |
1079 | -# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
1080 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
1081 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1082 | |
1083 | """Import version control metadata from a Bazaar branch into the database.""" |
1084 | @@ -34,7 +34,9 @@ |
1085 | from lp.code.model.revision import Revision |
1086 | from lp.codehosting.scanner import events |
1087 | from lp.services.config import config |
1088 | +from lp.services.features import getFeatureFlag |
1089 | from lp.services.utils import iter_list_chunks |
1090 | +from lp.services.webhooks.interfaces import IWebhookSet |
1091 | from lp.translations.interfaces.translationtemplatesbuild import ( |
1092 | ITranslationTemplatesBuildSource, |
1093 | ) |
1094 | @@ -323,3 +325,13 @@ |
1095 | |
1096 | def update_recipes(tip_changed): |
1097 | tip_changed.db_branch.markRecipesStale() |
1098 | + |
1099 | + |
1100 | +def trigger_webhooks(tip_changed): |
1101 | + old_revid = tip_changed.old_tip_revision_id |
1102 | + new_revid = tip_changed.new_tip_revision_id |
1103 | + if getFeatureFlag("code.bzr.webhooks.enabled") and old_revid != new_revid: |
1104 | + payload = tip_changed.composeWebhookPayload( |
1105 | + tip_changed.db_branch, old_revid, new_revid) |
1106 | + getUtility(IWebhookSet).trigger( |
1107 | + tip_changed.db_branch, "bzr:push:0.1", payload) |
1108 | |
1109 | === modified file 'lib/lp/codehosting/scanner/events.py' |
1110 | --- lib/lp/codehosting/scanner/events.py 2015-07-08 16:05:11 +0000 |
1111 | +++ lib/lp/codehosting/scanner/events.py 2015-09-29 15:54:57 +0000 |
1112 | @@ -1,4 +1,4 @@ |
1113 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
1114 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
1115 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1116 | |
1117 | """Events generated by the scanner.""" |
1118 | @@ -82,6 +82,14 @@ |
1119 | """The new tip revision id from this scan.""" |
1120 | return self.bzr_branch.last_revision() |
1121 | |
1122 | + @staticmethod |
1123 | + def composeWebhookPayload(branch, old_revid, new_revid): |
1124 | + return { |
1125 | + "bzr_branch_path": branch.shortened_path, |
1126 | + "old": {"revision_id": old_revid}, |
1127 | + "new": {"revision_id": new_revid}, |
1128 | + } |
1129 | + |
1130 | |
1131 | class IRevisionsRemoved(IObjectEvent): |
1132 | """Revisions have been removed from the branch.""" |
1133 | |
1134 | === modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py' |
1135 | --- lib/lp/codehosting/scanner/tests/test_bzrsync.py 2013-07-04 07:58:00 +0000 |
1136 | +++ lib/lp/codehosting/scanner/tests/test_bzrsync.py 2015-09-29 15:54:57 +0000 |
1137 | @@ -1,6 +1,6 @@ |
1138 | #!/usr/bin/python |
1139 | # |
1140 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
1141 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
1142 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1143 | |
1144 | import datetime |
1145 | @@ -17,6 +17,11 @@ |
1146 | from fixtures import TempDir |
1147 | import pytz |
1148 | from storm.locals import Store |
1149 | +from testtools.matchers import ( |
1150 | + Equals, |
1151 | + MatchesDict, |
1152 | + MatchesStructure, |
1153 | + ) |
1154 | from twisted.python.util import mergeFunctionMetadata |
1155 | from zope.component import getUtility |
1156 | from zope.security.proxy import removeSecurityProxy |
1157 | @@ -44,6 +49,7 @@ |
1158 | from lp.codehosting.scanner.bzrsync import BzrSync |
1159 | from lp.services.config import config |
1160 | from lp.services.database.interfaces import IStore |
1161 | +from lp.services.features.testing import FeatureFixture |
1162 | from lp.services.osutils import override_environ |
1163 | from lp.testing import TestCaseWithFactory |
1164 | from lp.testing.dbuser import ( |
1165 | @@ -743,6 +749,32 @@ |
1166 | self.assertEqual(False, recipe.is_stale) |
1167 | |
1168 | |
1169 | +class TestTriggerWebhooks(BzrSyncTestCase): |
1170 | + """Test triggering of webhooks.""" |
1171 | + |
1172 | + def test_triggers_webhooks(self): |
1173 | + # On tip change, any relevant webhooks are triggered. |
1174 | + self.useFixture(FeatureFixture({"code.bzr.webhooks.enabled": "on"})) |
1175 | + self.syncAndCount() |
1176 | + old_revid = self.db_branch.last_scanned_id |
1177 | + with dbuser(config.launchpad.dbuser): |
1178 | + hook = self.factory.makeWebhook( |
1179 | + target=self.db_branch, event_types=["bzr:push:0.1"]) |
1180 | + self.commitRevision() |
1181 | + new_revid = self.bzr_branch.last_revision() |
1182 | + self.makeBzrSync(self.db_branch).syncBranchAndClose() |
1183 | + delivery = hook.deliveries.one() |
1184 | + self.assertThat( |
1185 | + delivery, |
1186 | + MatchesStructure( |
1187 | + event_type=Equals("bzr:push:0.1"), |
1188 | + payload=MatchesDict({ |
1189 | + "bzr_branch_path": Equals(self.db_branch.shortened_path), |
1190 | + "old": Equals({"revision_id": old_revid}), |
1191 | + "new": Equals({"revision_id": new_revid}), |
1192 | + }))) |
1193 | + |
1194 | + |
1195 | class TestRevisionProperty(BzrSyncTestCase): |
1196 | """Tests for storting revision properties.""" |
1197 | |
1198 | |
1199 | === modified file 'lib/lp/registry/browser/productseries.py' |
1200 | --- lib/lp/registry/browser/productseries.py 2015-07-15 03:58:56 +0000 |
1201 | +++ lib/lp/registry/browser/productseries.py 2015-09-29 15:54:57 +0000 |
1202 | @@ -417,7 +417,8 @@ |
1203 | @property |
1204 | def long_bzr_identity(self): |
1205 | """The bzr identity of the branch including the unique_name.""" |
1206 | - return self.context.branch.branchIdentities()[-1][0] |
1207 | + lp_prefix = config.codehosting.bzr_lp_prefix |
1208 | + return lp_prefix + self.context.branch.getBranchIdentities()[-1][0] |
1209 | |
1210 | @property |
1211 | def is_obsolete(self): |
1212 | |
1213 | === modified file 'lib/lp/services/webhooks/client.py' |
1214 | --- lib/lp/services/webhooks/client.py 2015-09-01 06:03:55 +0000 |
1215 | +++ lib/lp/services/webhooks/client.py 2015-09-29 15:54:57 +0000 |
1216 | @@ -1,7 +1,7 @@ |
1217 | # Copyright 2015 Canonical Ltd. This software is licensed under the |
1218 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1219 | |
1220 | -"""Communication with the Git hosting service.""" |
1221 | +"""Communication with webhook delivery endpoints.""" |
1222 | |
1223 | __metaclass__ = type |
1224 | __all__ = [ |
1225 | |
1226 | === modified file 'lib/lp/services/webhooks/interfaces.py' |
1227 | --- lib/lp/services/webhooks/interfaces.py 2015-09-09 06:11:43 +0000 |
1228 | +++ lib/lp/services/webhooks/interfaces.py 2015-09-29 15:54:57 +0000 |
1229 | @@ -71,13 +71,14 @@ |
1230 | |
1231 | |
1232 | WEBHOOK_EVENT_TYPES = { |
1233 | + "bzr:push:0.1": "Bazaar push", |
1234 | "git:push:0.1": "Git push", |
1235 | } |
1236 | |
1237 | |
1238 | @error_status(httplib.UNAUTHORIZED) |
1239 | class WebhookFeatureDisabled(Exception): |
1240 | - """Only certain users can create new Git repositories.""" |
1241 | + """Only certain users can create new webhooks.""" |
1242 | |
1243 | def __init__(self): |
1244 | Exception.__init__( |
1245 | |
1246 | === modified file 'lib/lp/services/webhooks/model.py' |
1247 | --- lib/lp/services/webhooks/model.py 2015-09-09 06:11:43 +0000 |
1248 | +++ lib/lp/services/webhooks/model.py 2015-09-29 15:54:57 +0000 |
1249 | @@ -93,6 +93,9 @@ |
1250 | git_repository_id = Int(name='git_repository') |
1251 | git_repository = Reference(git_repository_id, 'GitRepository.id') |
1252 | |
1253 | + branch_id = Int(name='branch') |
1254 | + branch = Reference(branch_id, 'Branch.id') |
1255 | + |
1256 | registrant_id = Int(name='registrant', allow_none=False) |
1257 | registrant = Reference(registrant_id, 'Person.id') |
1258 | date_created = DateTime(tzinfo=utc, allow_none=False) |
1259 | @@ -108,6 +111,8 @@ |
1260 | def target(self): |
1261 | if self.git_repository is not None: |
1262 | return self.git_repository |
1263 | + elif self.branch is not None: |
1264 | + return self.branch |
1265 | else: |
1266 | raise AssertionError("No target.") |
1267 | |
1268 | @@ -159,10 +164,13 @@ |
1269 | |
1270 | def new(self, target, registrant, delivery_url, event_types, active, |
1271 | secret): |
1272 | + from lp.code.interfaces.branch import IBranch |
1273 | from lp.code.interfaces.gitrepository import IGitRepository |
1274 | hook = Webhook() |
1275 | if IGitRepository.providedBy(target): |
1276 | hook.git_repository = target |
1277 | + elif IBranch.providedBy(target): |
1278 | + hook.branch = target |
1279 | else: |
1280 | raise AssertionError("Unsupported target: %r" % (target,)) |
1281 | hook.registrant = registrant |
1282 | @@ -184,9 +192,12 @@ |
1283 | return IStore(Webhook).get(Webhook, id) |
1284 | |
1285 | def findByTarget(self, target): |
1286 | + from lp.code.interfaces.branch import IBranch |
1287 | from lp.code.interfaces.gitrepository import IGitRepository |
1288 | if IGitRepository.providedBy(target): |
1289 | target_filter = Webhook.git_repository == target |
1290 | + elif IBranch.providedBy(target): |
1291 | + target_filter = Webhook.branch == target |
1292 | else: |
1293 | raise AssertionError("Unsupported target: %r" % (target,)) |
1294 | return IStore(Webhook).find(Webhook, target_filter).order_by( |
1295 | |
1296 | === modified file 'lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt' |
1297 | --- lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-08-06 00:46:39 +0000 |
1298 | +++ lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-09-29 15:54:57 +0000 |
1299 | @@ -18,8 +18,8 @@ |
1300 | <div> |
1301 | <div class="beta" style="display: inline"> |
1302 | <img class="beta" alt="[BETA]" src="/@@/beta" /></div> |
1303 | - The only currently supported events are Git pushes. We'll be |
1304 | - rolling out webhooks for more soon. |
1305 | + The only currently supported events are Git and Bazaar pushes. We'll |
1306 | + be rolling out webhooks for more soon. |
1307 | </div> |
1308 | <ul class="horizontal"> |
1309 | <li> |
1310 | |
1311 | === modified file 'lib/lp/services/webhooks/tests/test_browser.py' |
1312 | --- lib/lp/services/webhooks/tests/test_browser.py 2015-08-10 05:56:25 +0000 |
1313 | +++ lib/lp/services/webhooks/tests/test_browser.py 2015-09-29 15:54:57 +0000 |
1314 | @@ -27,6 +27,7 @@ |
1315 | from lp.testing.matchers import HasQueryCount |
1316 | from lp.testing.views import create_view |
1317 | |
1318 | + |
1319 | breadcrumbs_tag = soupmatchers.Tag( |
1320 | 'breadcrumbs', 'ol', attrs={'class': 'breadcrumbs'}) |
1321 | webhooks_page_crumb_tag = soupmatchers.Tag( |
1322 | @@ -47,12 +48,28 @@ |
1323 | 'batch nav links', 'td', attrs={'class': 'batch-navigation-links'}) |
1324 | |
1325 | |
1326 | +class GitRepositoryTestHelpers: |
1327 | + |
1328 | + event_type = "git:push:0.1" |
1329 | + |
1330 | + def makeTarget(self): |
1331 | + return self.factory.makeGitRepository() |
1332 | + |
1333 | + |
1334 | +class BranchTestHelpers: |
1335 | + |
1336 | + event_type = "bzr:push:0.1" |
1337 | + |
1338 | + def makeTarget(self): |
1339 | + return self.factory.makeBranch() |
1340 | + |
1341 | + |
1342 | class WebhookTargetViewTestHelpers: |
1343 | |
1344 | def setUp(self): |
1345 | super(WebhookTargetViewTestHelpers, self).setUp() |
1346 | self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'})) |
1347 | - self.target = self.factory.makeGitRepository() |
1348 | + self.target = self.makeTarget() |
1349 | self.owner = self.target.owner |
1350 | login_person(self.owner) |
1351 | |
1352 | @@ -65,7 +82,7 @@ |
1353 | return view |
1354 | |
1355 | |
1356 | -class TestWebhooksView(WebhookTargetViewTestHelpers, TestCaseWithFactory): |
1357 | +class TestWebhooksViewBase(WebhookTargetViewTestHelpers): |
1358 | |
1359 | layer = DatabaseFunctionalLayer |
1360 | |
1361 | @@ -125,7 +142,19 @@ |
1362 | self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count))) |
1363 | |
1364 | |
1365 | -class TestWebhookAddView(WebhookTargetViewTestHelpers, TestCaseWithFactory): |
1366 | +class TestWebhooksViewGitRepository( |
1367 | + TestWebhooksViewBase, GitRepositoryTestHelpers, TestCaseWithFactory): |
1368 | + |
1369 | + pass |
1370 | + |
1371 | + |
1372 | +class TestWebhooksViewBranch( |
1373 | + TestWebhooksViewBase, BranchTestHelpers, TestCaseWithFactory): |
1374 | + |
1375 | + pass |
1376 | + |
1377 | + |
1378 | +class TestWebhookAddViewBase(WebhookTargetViewTestHelpers): |
1379 | |
1380 | layer = DatabaseFunctionalLayer |
1381 | |
1382 | @@ -150,7 +179,7 @@ |
1383 | form={ |
1384 | "field.delivery_url": "http://example.com/test", |
1385 | "field.active": "on", "field.event_types-empty-marker": "1", |
1386 | - "field.event_types": "git:push:0.1", |
1387 | + "field.event_types": self.event_type, |
1388 | "field.actions.new": "Add webhook"}) |
1389 | self.assertEqual([], view.errors) |
1390 | hook = self.target.webhooks.one() |
1391 | @@ -161,7 +190,7 @@ |
1392 | registrant=self.owner, |
1393 | delivery_url="http://example.com/test", |
1394 | active=True, |
1395 | - event_types=["git:push:0.1"])) |
1396 | + event_types=[self.event_type])) |
1397 | |
1398 | def test_rejects_bad_scheme(self): |
1399 | transaction.commit() |
1400 | @@ -176,12 +205,24 @@ |
1401 | self.assertIs(None, self.target.webhooks.one()) |
1402 | |
1403 | |
1404 | +class TestWebhookAddViewGitRepository( |
1405 | + TestWebhookAddViewBase, GitRepositoryTestHelpers, TestCaseWithFactory): |
1406 | + |
1407 | + pass |
1408 | + |
1409 | + |
1410 | +class TestWebhookAddViewBranch( |
1411 | + TestWebhookAddViewBase, BranchTestHelpers, TestCaseWithFactory): |
1412 | + |
1413 | + pass |
1414 | + |
1415 | + |
1416 | class WebhookViewTestHelpers: |
1417 | |
1418 | def setUp(self): |
1419 | super(WebhookViewTestHelpers, self).setUp() |
1420 | self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'})) |
1421 | - self.target = self.factory.makeGitRepository() |
1422 | + self.target = self.makeTarget() |
1423 | self.owner = self.target.owner |
1424 | self.webhook = self.factory.makeWebhook( |
1425 | target=self.target, delivery_url=u'http://example.com/original') |
1426 | @@ -196,7 +237,7 @@ |
1427 | return view |
1428 | |
1429 | |
1430 | -class TestWebhookView(WebhookViewTestHelpers, TestCaseWithFactory): |
1431 | +class TestWebhookViewBase(WebhookViewTestHelpers): |
1432 | |
1433 | layer = DatabaseFunctionalLayer |
1434 | |
1435 | @@ -249,7 +290,19 @@ |
1436 | event_types=[])) |
1437 | |
1438 | |
1439 | -class TestWebhookDeleteView(WebhookViewTestHelpers, TestCaseWithFactory): |
1440 | +class TestWebhookViewGitRepository( |
1441 | + TestWebhookViewBase, GitRepositoryTestHelpers, TestCaseWithFactory): |
1442 | + |
1443 | + pass |
1444 | + |
1445 | + |
1446 | +class TestWebhookViewBranch( |
1447 | + TestWebhookViewBase, BranchTestHelpers, TestCaseWithFactory): |
1448 | + |
1449 | + pass |
1450 | + |
1451 | + |
1452 | +class TestWebhookDeleteViewBase(WebhookViewTestHelpers): |
1453 | |
1454 | layer = DatabaseFunctionalLayer |
1455 | |
1456 | @@ -281,3 +334,15 @@ |
1457 | form={"field.actions.delete": "Delete webhook"}) |
1458 | self.assertEqual([], view.errors) |
1459 | self.assertIs(None, self.target.webhooks.one()) |
1460 | + |
1461 | + |
1462 | +class TestWebhookDeleteViewGitRepository( |
1463 | + TestWebhookDeleteViewBase, GitRepositoryTestHelpers, TestCaseWithFactory): |
1464 | + |
1465 | + pass |
1466 | + |
1467 | + |
1468 | +class TestWebhookDeleteViewBranch( |
1469 | + TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory): |
1470 | + |
1471 | + pass |
1472 | |
1473 | === modified file 'lib/lp/services/webhooks/tests/test_model.py' |
1474 | --- lib/lp/services/webhooks/tests/test_model.py 2015-08-10 07:36:52 +0000 |
1475 | +++ lib/lp/services/webhooks/tests/test_model.py 2015-09-29 15:54:57 +0000 |
1476 | @@ -115,17 +115,17 @@ |
1477 | expected_set_permissions, checker.set_permissions, 'set') |
1478 | |
1479 | |
1480 | -class TestWebhookSet(TestCaseWithFactory): |
1481 | +class TestWebhookSetBase: |
1482 | |
1483 | layer = DatabaseFunctionalLayer |
1484 | |
1485 | def test_new(self): |
1486 | - target = self.factory.makeGitRepository() |
1487 | + target = self.makeTarget() |
1488 | login_person(target.owner) |
1489 | person = self.factory.makePerson() |
1490 | hook = getUtility(IWebhookSet).new( |
1491 | - target, person, u'http://path/to/something', ['git:push'], True, |
1492 | - u'sekrit') |
1493 | + target, person, u'http://path/to/something', [self.event_type], |
1494 | + True, u'sekrit') |
1495 | Store.of(hook).flush() |
1496 | self.assertEqual(target, hook.target) |
1497 | self.assertEqual(person, hook.registrant) |
1498 | @@ -134,7 +134,7 @@ |
1499 | self.assertEqual(u'http://path/to/something', hook.delivery_url) |
1500 | self.assertEqual(True, hook.active) |
1501 | self.assertEqual(u'sekrit', hook.secret) |
1502 | - self.assertEqual(['git:push'], hook.event_types) |
1503 | + self.assertEqual([self.event_type], hook.event_types) |
1504 | |
1505 | def test_getByID(self): |
1506 | hook1 = self.factory.makeWebhook() |
1507 | @@ -148,8 +148,8 @@ |
1508 | None, getUtility(IWebhookSet).getByID(1234)) |
1509 | |
1510 | def test_findByTarget(self): |
1511 | - target1 = self.factory.makeGitRepository() |
1512 | - target2 = self.factory.makeGitRepository() |
1513 | + target1 = self.makeTarget() |
1514 | + target2 = self.makeTarget() |
1515 | for target, name in ((target1, 'one'), (target2, 'two')): |
1516 | for i in range(3): |
1517 | self.factory.makeWebhook( |
1518 | @@ -168,7 +168,7 @@ |
1519 | getUtility(IWebhookSet).findByTarget(target2)]) |
1520 | |
1521 | def test_delete(self): |
1522 | - target = self.factory.makeGitRepository() |
1523 | + target = self.makeTarget() |
1524 | login_person(target.owner) |
1525 | hooks = [] |
1526 | for i in range(3): |
1527 | @@ -195,21 +195,21 @@ |
1528 | |
1529 | def test_trigger(self): |
1530 | owner = self.factory.makePerson() |
1531 | - target1 = self.factory.makeGitRepository(owner=owner) |
1532 | - target2 = self.factory.makeGitRepository(owner=owner) |
1533 | + target1 = self.makeTarget(owner=owner) |
1534 | + target2 = self.makeTarget(owner=owner) |
1535 | hook1a = self.factory.makeWebhook( |
1536 | target=target1, event_types=[]) |
1537 | hook1b = self.factory.makeWebhook( |
1538 | - target=target1, event_types=['git:push:0.1']) |
1539 | + target=target1, event_types=[self.event_type]) |
1540 | hook2a = self.factory.makeWebhook( |
1541 | - target=target2, event_types=['git:push:0.1']) |
1542 | + target=target2, event_types=[self.event_type]) |
1543 | hook2b = self.factory.makeWebhook( |
1544 | - target=target2, event_types=['git:push:0.1'], active=False) |
1545 | + target=target2, event_types=[self.event_type], active=False) |
1546 | |
1547 | # Only webhooks subscribed to the relevant target and event type |
1548 | # are triggered. |
1549 | getUtility(IWebhookSet).trigger( |
1550 | - target1, 'git:push:0.1', {'some': 'payload'}) |
1551 | + target1, self.event_type, {'some': 'payload'}) |
1552 | with admin_logged_in(): |
1553 | self.assertThat(list(hook1a.deliveries), HasLength(0)) |
1554 | self.assertThat(list(hook1b.deliveries), HasLength(1)) |
1555 | @@ -220,7 +220,7 @@ |
1556 | |
1557 | # Disabled webhooks aren't triggered. |
1558 | getUtility(IWebhookSet).trigger( |
1559 | - target2, 'git:push:0.1', {'other': 'payload'}) |
1560 | + target2, self.event_type, {'other': 'payload'}) |
1561 | with admin_logged_in(): |
1562 | self.assertThat(list(hook1a.deliveries), HasLength(0)) |
1563 | self.assertThat(list(hook1b.deliveries), HasLength(1)) |
1564 | @@ -228,3 +228,19 @@ |
1565 | self.assertThat(list(hook2b.deliveries), HasLength(0)) |
1566 | delivery = hook2a.deliveries.one() |
1567 | self.assertEqual(delivery.payload, {'other': 'payload'}) |
1568 | + |
1569 | + |
1570 | +class TestWebhookSetGitRepository(TestWebhookSetBase, TestCaseWithFactory): |
1571 | + |
1572 | + event_type = 'git:push:0.1' |
1573 | + |
1574 | + def makeTarget(self, owner=None): |
1575 | + return self.factory.makeGitRepository(owner=owner) |
1576 | + |
1577 | + |
1578 | +class TestWebhookSetBranch(TestWebhookSetBase, TestCaseWithFactory): |
1579 | + |
1580 | + event_type = 'bzr:push:0.1' |
1581 | + |
1582 | + def makeTarget(self, owner=None): |
1583 | + return self.factory.makeBranch(owner=owner) |
1584 | |
1585 | === modified file 'lib/lp/services/webhooks/tests/test_webservice.py' |
1586 | --- lib/lp/services/webhooks/tests/test_webservice.py 2015-09-09 06:11:43 +0000 |
1587 | +++ lib/lp/services/webhooks/tests/test_webservice.py 2015-09-29 15:54:57 +0000 |
1588 | @@ -260,12 +260,12 @@ |
1589 | self.assertIs(None, representation['date_first_sent']) |
1590 | |
1591 | |
1592 | -class TestWebhookTarget(TestCaseWithFactory): |
1593 | +class TestWebhookTargetBase: |
1594 | layer = DatabaseFunctionalLayer |
1595 | |
1596 | def setUp(self): |
1597 | - super(TestWebhookTarget, self).setUp() |
1598 | - self.target = self.factory.makeGitRepository() |
1599 | + super(TestWebhookTargetBase, self).setUp() |
1600 | + self.target = self.makeTarget() |
1601 | self.owner = self.target.owner |
1602 | self.target_url = api_url(self.target) |
1603 | self.webservice = webservice_for_person( |
1604 | @@ -309,13 +309,13 @@ |
1605 | response = self.webservice.named_post( |
1606 | self.target_url, 'newWebhook', |
1607 | delivery_url='http://example.com/ep', |
1608 | - event_types=['git:push:0.1'], api_version='devel') |
1609 | + event_types=[self.event_type], api_version='devel') |
1610 | self.assertEqual(201, response.status) |
1611 | |
1612 | representation = self.webservice.get( |
1613 | self.target_url + '/webhooks', api_version='devel').jsonBody() |
1614 | self.assertContentEqual( |
1615 | - [('http://example.com/ep', ['git:push:0.1'], True)], |
1616 | + [('http://example.com/ep', [self.event_type], True)], |
1617 | [(entry['delivery_url'], entry['event_types'], entry['active']) |
1618 | for entry in representation['entries']]) |
1619 | |
1620 | @@ -323,8 +323,9 @@ |
1621 | self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'})) |
1622 | response = self.webservice.named_post( |
1623 | self.target_url, 'newWebhook', |
1624 | - delivery_url='http://example.com/ep', event_types=['git:push:0.1'], |
1625 | - secret='sekrit', api_version='devel') |
1626 | + delivery_url='http://example.com/ep', |
1627 | + event_types=[self.event_type], secret='sekrit', |
1628 | + api_version='devel') |
1629 | self.assertEqual(201, response.status) |
1630 | |
1631 | # The secret is set, but cannot be read back through the API. |
1632 | @@ -339,16 +340,33 @@ |
1633 | webservice = LaunchpadWebServiceCaller() |
1634 | response = webservice.named_post( |
1635 | self.target_url, 'newWebhook', |
1636 | - delivery_url='http://example.com/ep', event_types=['git:push:0.1'], |
1637 | - api_version='devel') |
1638 | + delivery_url='http://example.com/ep', |
1639 | + event_types=[self.event_type], api_version='devel') |
1640 | self.assertEqual(401, response.status) |
1641 | self.assertIn('launchpad.Edit', response.body) |
1642 | |
1643 | def test_newWebhook_feature_flag_guard(self): |
1644 | response = self.webservice.named_post( |
1645 | self.target_url, 'newWebhook', |
1646 | - delivery_url='http://example.com/ep', event_types=['git:push:0.1'], |
1647 | - api_version='devel') |
1648 | + delivery_url='http://example.com/ep', |
1649 | + event_types=[self.event_type], api_version='devel') |
1650 | self.assertEqual(401, response.status) |
1651 | self.assertEqual( |
1652 | 'This webhook feature is not available yet.', response.body) |
1653 | + |
1654 | + |
1655 | +class TestWebhookTargetGitRepository( |
1656 | + TestWebhookTargetBase, TestCaseWithFactory): |
1657 | + |
1658 | + event_type = 'git:push:0.1' |
1659 | + |
1660 | + def makeTarget(self): |
1661 | + return self.factory.makeGitRepository() |
1662 | + |
1663 | + |
1664 | +class TestWebhookTargetBranch(TestWebhookTargetBase, TestCaseWithFactory): |
1665 | + |
1666 | + event_type = 'bzr:push:0.1' |
1667 | + |
1668 | + def makeTarget(self): |
1669 | + return self.factory.makeBranch() |
All right, in the name of consistency I cleaned up a bunch of bits of Branch (mainly BzrIdentityMixin) to match the corresponding newer code in GitRepository/ GitIdentityMixi n, and introduced Branch. shortened_ path which lacks the lp: prefix. bzr_branch_path for this branch would now be "~cjwatson/ launchpad/ bzr-webhooks" , or for the target of this merge proposal would be "launchpad". Does this look OK to you?