Merge lp:~cjwatson/launchpad/bzr-webhooks into lp:launchpad

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
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 :

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/GitIdentityMixin, 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?

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.getByUrl exists, but there's no lp.branches.getByPath.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2015-09-23 15:38:13 +0000
+++ lib/lp/code/browser/branch.py 2015-09-29 15:54:57 +0000
@@ -125,6 +125,7 @@
125from lp.services import searchbuilder125from lp.services import searchbuilder
126from lp.services.config import config126from lp.services.config import config
127from lp.services.database.constants import UTC_NOW127from lp.services.database.constants import UTC_NOW
128from lp.services.features import getFeatureFlag
128from lp.services.feeds.browser import (129from lp.services.feeds.browser import (
129 BranchFeedLink,130 BranchFeedLink,
130 FeedsMixin,131 FeedsMixin,
@@ -152,6 +153,7 @@
152from lp.services.webapp.breadcrumb import NameBreadcrumb153from lp.services.webapp.breadcrumb import NameBreadcrumb
153from lp.services.webapp.escaping import structured154from lp.services.webapp.escaping import structured
154from lp.services.webapp.interfaces import ICanonicalUrlData155from lp.services.webapp.interfaces import ICanonicalUrlData
156from lp.services.webhooks.browser import WebhookTargetNavigationMixin
155from lp.snappy.browser.hassnaps import (157from lp.snappy.browser.hassnaps import (
156 HasSnapsMenuMixin,158 HasSnapsMenuMixin,
157 HasSnapsViewMixin,159 HasSnapsViewMixin,
@@ -183,7 +185,7 @@
183 return self.context.target.components[-1]185 return self.context.target.components[-1]
184186
185187
186class BranchNavigation(Navigation):188class BranchNavigation(WebhookTargetNavigationMixin, Navigation):
187189
188 usedfor = IBranch190 usedfor = IBranch
189191
@@ -240,7 +242,7 @@
240 facet = 'branches'242 facet = 'branches'
241 title = 'Edit branch'243 title = 'Edit branch'
242 links = (244 links = (
243 'edit', 'reviewer', 'edit_whiteboard', 'delete')245 'edit', 'reviewer', 'edit_whiteboard', 'webhooks', 'delete')
244246
245 def branch_is_import(self):247 def branch_is_import(self):
246 return self.context.branch_type == BranchType.IMPORTED248 return self.context.branch_type == BranchType.IMPORTED
@@ -267,6 +269,13 @@
267 text = 'Set branch reviewer'269 text = 'Set branch reviewer'
268 return Link('+reviewer', text, icon='edit')270 return Link('+reviewer', text, icon='edit')
269271
272 @enabled_with_permission('launchpad.Edit')
273 def webhooks(self):
274 text = 'Manage webhooks'
275 return Link(
276 '+webhooks', text, icon='edit',
277 enabled=bool(getFeatureFlag('webhooks.new.enabled')))
278
270279
271class BranchContextMenu(ContextMenu, HasRecipesMenuMixin, HasSnapsMenuMixin):280class BranchContextMenu(ContextMenu, HasRecipesMenuMixin, HasSnapsMenuMixin):
272 """Context menu for branches."""281 """Context menu for branches."""
273282
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-09-11 15:11:34 +0000
+++ lib/lp/code/configure.zcml 2015-09-29 15:54:57 +0000
@@ -418,6 +418,9 @@
418 for="lp.codehosting.scanner.events.ITipChanged"418 for="lp.codehosting.scanner.events.ITipChanged"
419 handler="lp.codehosting.scanner.bzrsync.update_recipes"/>419 handler="lp.codehosting.scanner.bzrsync.update_recipes"/>
420 <subscriber420 <subscriber
421 for="lp.codehosting.scanner.events.ITipChanged"
422 handler="lp.codehosting.scanner.bzrsync.trigger_webhooks"/>
423 <subscriber
421 for="lp.codehosting.scanner.events.IRevisionsRemoved"424 for="lp.codehosting.scanner.events.IRevisionsRemoved"
422 handler="lp.codehosting.scanner.email.send_removed_revision_emails"/>425 handler="lp.codehosting.scanner.email.send_removed_revision_emails"/>
423 <subscriber426 <subscriber
424427
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2015-09-29 00:32:52 +0000
+++ lib/lp/code/interfaces/branch.py 2015-09-29 15:54:57 +0000
@@ -105,6 +105,7 @@
105 structured,105 structured,
106 )106 )
107from lp.services.webapp.interfaces import ITableBatchNavigator107from lp.services.webapp.interfaces import ITableBatchNavigator
108from lp.services.webhooks.interfaces import IWebhookTarget
108109
109110
110DEFAULT_BRANCH_STATUS_IN_LISTING = (111DEFAULT_BRANCH_STATUS_IN_LISTING = (
@@ -711,6 +712,10 @@
711 Reference(712 Reference(
712 title=_("The associated CodeImport, if any."), schema=Interface))713 title=_("The associated CodeImport, if any."), schema=Interface))
713714
715 shortened_path = Attribute(
716 "The shortest reasonable version of the path to this branch; as "
717 "bzr_identity but without the 'lp:' prefix.")
718
714 bzr_identity = exported(719 bzr_identity = exported(
715 Text(720 Text(
716 title=_('Bazaar Identity'),721 title=_('Bazaar Identity'),
@@ -780,7 +785,7 @@
780 :return: A list of suite source packages ordered by pocket.785 :return: A list of suite source packages ordered by pocket.
781 """786 """
782787
783 def branchLinks():788 def getBranchLinks():
784 """Return a sorted list of ICanHasLinkedBranch objects.789 """Return a sorted list of ICanHasLinkedBranch objects.
785790
786 There is one result for each related linked object that the branch is791 There is one result for each related linked object that the branch is
@@ -792,7 +797,7 @@
792 more important links are sorted first.797 more important links are sorted first.
793 """798 """
794799
795 def branchIdentities():800 def getBranchIdentities():
796 """A list of aliases for a branch.801 """A list of aliases for a branch.
797802
798 Returns a list of tuples of bzr identity and context object. There is803 Returns a list of tuples of bzr identity and context object. There is
@@ -805,10 +810,10 @@
805810
806 For example, a branch linked to the development focus of the 'fooix'811 For example, a branch linked to the development focus of the 'fooix'
807 project is accessible using:812 project is accessible using:
808 lp:fooix - the linked object is the product fooix813 fooix - the linked object is the product fooix
809 lp:fooix/trunk - the linked object is the trunk series of fooix814 fooix/trunk - the linked object is the trunk series of fooix
810 lp:~owner/fooix/name - the unique name of the branch where the815 ~owner/fooix/name - the unique name of the branch where the linked
811 linked object is the branch itself.816 object is the branch itself.
812 """817 """
813818
814 # subscription-related methods819 # subscription-related methods
@@ -1129,7 +1134,7 @@
1129 vocabulary=ControlFormat))1134 vocabulary=ControlFormat))
11301135
11311136
1132class IBranchEdit(Interface):1137class IBranchEdit(IWebhookTarget):
1133 """IBranch attributes that require launchpad.Edit permission."""1138 """IBranch attributes that require launchpad.Edit permission."""
11341139
1135 @call_with(user=REQUEST_USER)1140 @call_with(user=REQUEST_USER)
@@ -1313,8 +1318,7 @@
1313 Return None if no match was found.1318 Return None if no match was found.
1314 """1319 """
13151320
1316 @operation_parameters(1321 @operation_parameters(url=TextLine(title=_('Branch URL'), required=True))
1317 url=TextLine(title=_('Branch URL'), required=True))
1318 @operation_returns_entry(IBranch)1322 @operation_returns_entry(IBranch)
1319 @export_read_operation()1323 @export_read_operation()
1320 @operation_for_version('beta')1324 @operation_for_version('beta')
@@ -1357,6 +1361,29 @@
1357 associated branch, the URL will map to `None`.1361 associated branch, the URL will map to `None`.
1358 """1362 """
13591363
1364 @operation_parameters(path=TextLine(title=_('Branch path'), required=True))
1365 @operation_returns_entry(IBranch)
1366 @export_read_operation()
1367 @operation_for_version('devel')
1368 def getByPath(path):
1369 """Find a branch by its path.
1370
1371 The path is the same as its lp: URL, but without the leading lp:, so
1372 it may be in any of these forms::
1373
1374 Unique names:
1375 ~OWNER/PROJECT/NAME
1376 ~OWNER/DISTRO/SERIES/SOURCE/NAME
1377 ~OWNER/+junk/NAME
1378 Aliases linked to other objects:
1379 PROJECT
1380 PROJECT/SERIES
1381 DISTRO/SOURCE
1382 DISTRO/SUITE/SOURCE
1383
1384 Return None if no match was found.
1385 """
1386
1360 @collection_default_content()1387 @collection_default_content()
1361 def getBranches(limit=50, eager_load=True):1388 def getBranches(limit=50, eager_load=True):
1362 """Return a collection of branches.1389 """Return a collection of branches.
@@ -1481,26 +1508,29 @@
1481 """1508 """
14821509
1483 @property1510 @property
1511 def shortened_path(self):
1512 """See `IBranch`."""
1513 return self.getBranchIdentities()[0][0]
1514
1515 @property
1484 def bzr_identity(self):1516 def bzr_identity(self):
1485 """See `IBranch`."""1517 """See `IBranch`."""
1486 identity, context = self.branchIdentities()[0]1518 return config.codehosting.bzr_lp_prefix + self.shortened_path
1487 return identity
14881519
1489 identity = bzr_identity1520 identity = bzr_identity
14901521
1491 def branchIdentities(self):1522 def getBranchIdentities(self):
1492 """See `IBranch`."""1523 """See `IBranch`."""
1493 lp_prefix = config.codehosting.bzr_lp_prefix1524 if not self.target.supports_short_identities:
1494 if not self.target.supports_short_identites:
1495 identities = []1525 identities = []
1496 else:1526 else:
1497 identities = [1527 identities = [
1498 (lp_prefix + link.bzr_path, link.context)1528 (link.bzr_path, link.context)
1499 for link in self.branchLinks()]1529 for link in self.getBranchLinks()]
1500 identities.append((lp_prefix + self.unique_name, self))1530 identities.append((self.unique_name, self))
1501 return identities1531 return identities
15021532
1503 def branchLinks(self):1533 def getBranchLinks(self):
1504 """See `IBranch`."""1534 """See `IBranch`."""
1505 links = []1535 links = []
1506 for suite_sp in self.associatedSuiteSourcePackages():1536 for suite_sp in self.associatedSuiteSourcePackages():
15071537
=== modified file 'lib/lp/code/interfaces/branchlookup.py'
--- lib/lp/code/interfaces/branchlookup.py 2013-01-07 02:40:55 +0000
+++ lib/lp/code/interfaces/branchlookup.py 2015-09-29 15:54:57 +0000
@@ -158,6 +158,14 @@
158 paths are not handled for shortcut paths.158 paths are not handled for shortcut paths.
159 """159 """
160160
161 def getByPath(path):
162 """Find the branch associated with a path.
163
164 As with `getByLPPath`, but returns None instead of raising any of
165 the documented exceptions, and returns only the `IBranch` on success
166 and not any extra path.
167 """
168
161169
162def path_lookups(path):170def path_lookups(path):
163 if path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):171 if path.startswith(BRANCH_ID_ALIAS_PREFIX + '/'):
164172
=== modified file 'lib/lp/code/interfaces/branchtarget.py'
--- lib/lp/code/interfaces/branchtarget.py 2015-04-21 14:32:31 +0000
+++ lib/lp/code/interfaces/branchtarget.py 2015-09-29 15:54:57 +0000
@@ -92,7 +92,7 @@
92 supports_merge_proposals = Attribute(92 supports_merge_proposals = Attribute(
93 "Does this target support merge proposals at all?")93 "Does this target support merge proposals at all?")
9494
95 supports_short_identites = Attribute(95 supports_short_identities = Attribute(
96 "Does this target support shortened bazaar identities?")96 "Does this target support shortened bazaar identities?")
9797
98 supports_code_imports = Attribute(98 supports_code_imports = Attribute(
9999
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2015-09-21 10:08:08 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2015-09-29 15:54:57 +0000
@@ -688,7 +688,8 @@
688 def getByPath(user, path):688 def getByPath(user, path):
689 """Find a repository by its path.689 """Find a repository by its path.
690690
691 Any of these forms may be used, with or without a leading slash:691 Any of these forms may be used::
692
692 Unique names:693 Unique names:
693 ~OWNER/PROJECT/+git/NAME694 ~OWNER/PROJECT/+git/NAME
694 ~OWNER/DISTRO/+source/SOURCE/+git/NAME695 ~OWNER/DISTRO/+source/SOURCE/+git/NAME
695696
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2015-09-29 00:32:52 +0000
+++ lib/lp/code/model/branch.py 2015-09-29 15:54:57 +0000
@@ -180,10 +180,12 @@
180from lp.services.webapp import urlappend180from lp.services.webapp import urlappend
181from lp.services.webapp.authorization import check_permission181from lp.services.webapp.authorization import check_permission
182from lp.services.webapp.interfaces import ILaunchBag182from lp.services.webapp.interfaces import ILaunchBag
183from lp.services.webhooks.interfaces import IWebhookSet
184from lp.services.webhooks.model import WebhookTargetMixin
183185
184186
185@implementer(IBranch, IPrivacy, IInformationType)187@implementer(IBranch, IPrivacy, IInformationType)
186class Branch(SQLBase, BzrIdentityMixin):188class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
187 """A sequence of ordered revisions in Bazaar."""189 """A sequence of ordered revisions in Bazaar."""
188 _table = 'Branch'190 _table = 'Branch'
189191
@@ -1336,6 +1338,7 @@
13361338
1337 self._deleteBranchSubscriptions()1339 self._deleteBranchSubscriptions()
1338 self._deleteJobs()1340 self._deleteJobs()
1341 getUtility(IWebhookSet).delete(self.webhooks)
13391342
1340 # Now destroy the branch.1343 # Now destroy the branch.
1341 branch_id = self.id1344 branch_id = self.id
@@ -1610,6 +1613,10 @@
1610 """See `IBranchSet`."""1613 """See `IBranchSet`."""
1611 return getUtility(IBranchLookup).getByUrls(urls)1614 return getUtility(IBranchLookup).getByUrls(urls)
16121615
1616 def getByPath(self, path):
1617 """See `IBranchSet`."""
1618 return getUtility(IBranchLookup).getByPath(path)
1619
1613 def getBranches(self, limit=50, eager_load=True):1620 def getBranches(self, limit=50, eager_load=True):
1614 """See `IBranchSet`."""1621 """See `IBranchSet`."""
1615 anon_branches = getUtility(IAllBranches).visibleByUser(None)1622 anon_branches = getUtility(IAllBranches).visibleByUser(None)
16161623
=== modified file 'lib/lp/code/model/branchlookup.py'
--- lib/lp/code/model/branchlookup.py 2015-07-10 05:57:13 +0000
+++ lib/lp/code/model/branchlookup.py 2015-09-29 15:54:57 +0000
@@ -248,14 +248,7 @@
248 if uri.scheme == 'lp':248 if uri.scheme == 'lp':
249 if not self._uriHostAllowed(uri):249 if not self._uriHostAllowed(uri):
250 return None250 return None
251 try:251 return self.getByPath(uri.path.lstrip('/'))
252 return self.getByLPPath(uri.path.lstrip('/'))[0]
253 except (
254 CannotHaveLinkedBranch, InvalidNamespace, InvalidProductName,
255 NoSuchBranch, NoSuchPerson, NoSuchProduct,
256 NoSuchProductSeries, NoSuchDistroSeries,
257 NoSuchSourcePackageName, NoLinkedBranch):
258 return None
259252
260 return Branch.selectOneBy(url=url)253 return Branch.selectOneBy(url=url)
261254
@@ -386,6 +379,16 @@
386 suffix = path[len(bzr_path) + 1:]379 suffix = path[len(bzr_path) + 1:]
387 return branch, suffix380 return branch, suffix
388381
382 def getByPath(self, path):
383 """See `IBranchLookup`."""
384 try:
385 return self.getByLPPath(path)[0]
386 except (
387 CannotHaveLinkedBranch, InvalidNamespace, InvalidProductName,
388 NoSuchBranch, NoSuchPerson, NoSuchProduct, NoSuchProductSeries,
389 NoSuchDistroSeries, NoSuchSourcePackageName, NoLinkedBranch):
390 return None
391
389 def _getLinkedBranchAndPath(self, provided):392 def _getLinkedBranchAndPath(self, provided):
390 """Get the linked branch for 'provided', and the bzr_path.393 """Get the linked branch for 'provided', and the bzr_path.
391394
392395
=== modified file 'lib/lp/code/model/branchtarget.py'
--- lib/lp/code/model/branchtarget.py 2015-07-08 16:05:11 +0000
+++ lib/lp/code/model/branchtarget.py 2015-09-29 15:54:57 +0000
@@ -113,7 +113,7 @@
113 return True113 return True
114114
115 @property115 @property
116 def supports_short_identites(self):116 def supports_short_identities(self):
117 """See `IBranchTarget`."""117 """See `IBranchTarget`."""
118 return True118 return True
119119
@@ -229,7 +229,7 @@
229 return False229 return False
230230
231 @property231 @property
232 def supports_short_identites(self):232 def supports_short_identities(self):
233 """See `IBranchTarget`."""233 """See `IBranchTarget`."""
234 return False234 return False
235235
@@ -314,7 +314,7 @@
314 return True314 return True
315315
316 @property316 @property
317 def supports_short_identites(self):317 def supports_short_identities(self):
318 """See `IBranchTarget`."""318 """See `IBranchTarget`."""
319 return True319 return True
320320
321321
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2015-08-06 12:03:36 +0000
+++ lib/lp/code/model/tests/test_branch.py 2015-09-29 15:54:57 +0000
@@ -910,202 +910,8 @@
910 self.assertFalse(branch.upgrade_pending)910 self.assertFalse(branch.upgrade_pending)
911911
912912
913class TestBranchLinksAndIdentites(TestCaseWithFactory):913class TestBzrIdentityMixin(TestCaseWithFactory):
914 """Test IBranch.branchLinks and IBranch.branchIdentities."""914 """Test the defaults and identities provided by BzrIdentityMixin."""
915
916 layer = DatabaseFunctionalLayer
917
918 def test_default_identities(self):
919 # If there are no links, the only branch identity is the unique name.
920 branch = self.factory.makeAnyBranch()
921 self.assertEqual(
922 [('lp://dev/' + branch.unique_name, branch)],
923 branch.branchIdentities())
924
925 def test_linked_to_product(self):
926 # If a branch is linked to the product, it is also by definition
927 # linked to the development focus of the product.
928 fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
929 fooix.development_focus.name = 'devel'
930 eric = self.factory.makePerson(name='eric')
931 branch = self.factory.makeProductBranch(
932 product=fooix, owner=eric, name='trunk')
933 linked_branch = ICanHasLinkedBranch(fooix)
934 linked_branch.setBranch(branch)
935 self.assertEqual(
936 [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
937 branch.branchLinks())
938 self.assertEqual(
939 [('lp://dev/fooix', fooix),
940 ('lp://dev/fooix/devel', fooix.development_focus),
941 ('lp://dev/~eric/fooix/trunk', branch)],
942 branch.branchIdentities())
943
944 def test_linked_to_product_series(self):
945 # If a branch is linked to a non-development series of a product and
946 # not linked to the product itself, then only the product series is
947 # returned in the links.
948 fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
949 future = self.factory.makeProductSeries(product=fooix, name='future')
950 eric = self.factory.makePerson(name='eric')
951 branch = self.factory.makeProductBranch(
952 product=fooix, owner=eric, name='trunk')
953 linked_branch = ICanHasLinkedBranch(future)
954 login_person(fooix.owner)
955 linked_branch.setBranch(branch)
956 self.assertEqual(
957 [linked_branch],
958 branch.branchLinks())
959 self.assertEqual(
960 [('lp://dev/fooix/future', future),
961 ('lp://dev/~eric/fooix/trunk', branch)],
962 branch.branchIdentities())
963
964 def test_linked_to_package(self):
965 # If a branch is linked to a suite source package where the
966 # distroseries is the current series for the distribution, there is a
967 # link for both the distribution source package and the suite source
968 # package.
969 mint = self.factory.makeDistribution(name='mint')
970 dev = self.factory.makeDistroSeries(
971 distribution=mint, version='1.0', name='dev')
972 eric = self.factory.makePerson(name='eric')
973 branch = self.factory.makePackageBranch(
974 distroseries=dev, sourcepackagename='choc', name='tip',
975 owner=eric)
976 dsp = self.factory.makeDistributionSourcePackage('choc', mint)
977 distro_link = ICanHasLinkedBranch(dsp)
978 development_package = dsp.development_version
979 suite_sourcepackage = development_package.getSuiteSourcePackage(
980 PackagePublishingPocket.RELEASE)
981 suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
982
983 registrant = suite_sourcepackage.distribution.owner
984 run_with_login(
985 registrant,
986 suite_sp_link.setBranch, branch, registrant)
987
988 self.assertEqual(
989 [distro_link, suite_sp_link],
990 branch.branchLinks())
991 self.assertEqual(
992 [('lp://dev/mint/choc', dsp),
993 ('lp://dev/mint/dev/choc', suite_sourcepackage),
994 ('lp://dev/~eric/mint/dev/choc/tip', branch)],
995 branch.branchIdentities())
996
997 def test_linked_to_package_not_release_pocket(self):
998 # If a branch is linked to a suite source package where the
999 # distroseries is the current series for the distribution, but the
1000 # pocket is not the RELEASE pocket, then there is only the link for
1001 # the suite source package.
1002 mint = self.factory.makeDistribution(name='mint')
1003 dev = self.factory.makeDistroSeries(
1004 distribution=mint, version='1.0', name='dev')
1005 eric = self.factory.makePerson(name='eric')
1006 branch = self.factory.makePackageBranch(
1007 distroseries=dev, sourcepackagename='choc', name='tip',
1008 owner=eric)
1009 dsp = self.factory.makeDistributionSourcePackage('choc', mint)
1010 development_package = dsp.development_version
1011 suite_sourcepackage = development_package.getSuiteSourcePackage(
1012 PackagePublishingPocket.BACKPORTS)
1013 suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
1014
1015 registrant = suite_sourcepackage.distribution.owner
1016 run_with_login(
1017 registrant,
1018 suite_sp_link.setBranch, branch, registrant)
1019
1020 self.assertEqual(
1021 [suite_sp_link],
1022 branch.branchLinks())
1023 self.assertEqual(
1024 [('lp://dev/mint/dev-backports/choc', suite_sourcepackage),
1025 ('lp://dev/~eric/mint/dev/choc/tip', branch)],
1026 branch.branchIdentities())
1027
1028 def test_linked_to_package_not_current_series(self):
1029 # If the branch is linked to a suite source package where the distro
1030 # series is not the current series, only the suite source package is
1031 # returned in the links.
1032 mint = self.factory.makeDistribution(name='mint')
1033 self.factory.makeDistroSeries(
1034 distribution=mint, version='1.0', name='dev')
1035 supported = self.factory.makeDistroSeries(
1036 distribution=mint, version='0.9', name='supported')
1037 eric = self.factory.makePerson(name='eric')
1038 branch = self.factory.makePackageBranch(
1039 distroseries=supported, sourcepackagename='choc', name='tip',
1040 owner=eric)
1041 suite_sp = self.factory.makeSuiteSourcePackage(
1042 distroseries=supported, sourcepackagename='choc',
1043 pocket=PackagePublishingPocket.RELEASE)
1044 suite_sp_link = ICanHasLinkedBranch(suite_sp)
1045
1046 registrant = suite_sp.distribution.owner
1047 run_with_login(
1048 registrant,
1049 suite_sp_link.setBranch, branch, registrant)
1050
1051 self.assertEqual(
1052 [suite_sp_link],
1053 branch.branchLinks())
1054 self.assertEqual(
1055 [('lp://dev/mint/supported/choc', suite_sp),
1056 ('lp://dev/~eric/mint/supported/choc/tip', branch)],
1057 branch.branchIdentities())
1058
1059 def test_linked_across_project_to_package(self):
1060 # If a product branch is linked to a suite source package, the links
1061 # are the same as if it was a source package branch.
1062 mint = self.factory.makeDistribution(name='mint')
1063 self.factory.makeDistroSeries(
1064 distribution=mint, version='1.0', name='dev')
1065 eric = self.factory.makePerson(name='eric')
1066 fooix = self.factory.makeProduct(name='fooix')
1067 branch = self.factory.makeProductBranch(
1068 product=fooix, owner=eric, name='trunk')
1069 dsp = self.factory.makeDistributionSourcePackage('choc', mint)
1070 distro_link = ICanHasLinkedBranch(dsp)
1071 development_package = dsp.development_version
1072 suite_sourcepackage = development_package.getSuiteSourcePackage(
1073 PackagePublishingPocket.RELEASE)
1074 suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
1075
1076 registrant = suite_sourcepackage.distribution.owner
1077 run_with_login(
1078 registrant,
1079 suite_sp_link.setBranch, branch, registrant)
1080
1081 self.assertEqual(
1082 [distro_link, suite_sp_link],
1083 branch.branchLinks())
1084 self.assertEqual(
1085 [('lp://dev/mint/choc', dsp),
1086 ('lp://dev/mint/dev/choc', suite_sourcepackage),
1087 ('lp://dev/~eric/fooix/trunk', branch)],
1088 branch.branchIdentities())
1089
1090 def test_junk_branch_links(self):
1091 # If a junk branch has links, those links are returned in the
1092 # branchLinks, but the branchIdentities just has the branch unique
1093 # name.
1094 eric = self.factory.makePerson(name='eric')
1095 branch = self.factory.makePersonalBranch(owner=eric, name='foo')
1096 fooix = removeSecurityProxy(self.factory.makeProduct())
1097 linked_branch = ICanHasLinkedBranch(fooix)
1098 linked_branch.setBranch(branch)
1099 self.assertEqual(
1100 [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
1101 branch.branchLinks())
1102 self.assertEqual(
1103 [('lp://dev/~eric/+junk/foo', branch)],
1104 branch.branchIdentities())
1105
1106
1107class TestBzrIdentity(TestCaseWithFactory):
1108 """Test IBranch.bzr_identity."""
1109915
1110 layer = DatabaseFunctionalLayer916 layer = DatabaseFunctionalLayer
1111917
@@ -1115,16 +921,17 @@
1115 Actually, it'll be lp://dev/<identity_path>.921 Actually, it'll be lp://dev/<identity_path>.
1116 """922 """
1117 self.assertEqual(923 self.assertEqual(
1118 'lp://dev/%s' % identity_path, branch.bzr_identity,924 identity_path, branch.shortened_path, "shortened path")
1119 "bzr identity")925 self.assertEqual(
926 'lp://dev/%s' % identity_path, branch.bzr_identity, "bzr identity")
1120927
1121 def test_default_identity(self):928 def test_bzr_identity_default(self):
1122 # By default, the bzr identity is an lp URL with the branch's unique929 # By default, the bzr identity is an lp URL with the branch's unique
1123 # name.930 # name.
1124 branch = self.factory.makeAnyBranch()931 branch = self.factory.makeAnyBranch()
1125 self.assertBzrIdentity(branch, branch.unique_name)932 self.assertBzrIdentity(branch, branch.unique_name)
1126933
1127 def test_linked_to_product(self):934 def test_bzr_identity_linked_to_product(self):
1128 # If a branch is the development focus branch for a product, then it's935 # If a branch is the development focus branch for a product, then it's
1129 # bzr identity is lp:product.936 # bzr identity is lp:product.
1130 branch = self.factory.makeProductBranch()937 branch = self.factory.makeProductBranch()
@@ -1133,7 +940,7 @@
1133 linked_branch.setBranch(branch)940 linked_branch.setBranch(branch)
1134 self.assertBzrIdentity(branch, linked_branch.bzr_path)941 self.assertBzrIdentity(branch, linked_branch.bzr_path)
1135942
1136 def test_linked_to_product_series(self):943 def test_bzr_identity_linked_to_product_series(self):
1137 # If a branch is the development focus branch for a product series,944 # If a branch is the development focus branch for a product series,
1138 # then it's bzr identity is lp:product/series.945 # then it's bzr identity is lp:product/series.
1139 branch = self.factory.makeProductBranch()946 branch = self.factory.makeProductBranch()
@@ -1144,7 +951,7 @@
1144 linked_branch.setBranch(branch)951 linked_branch.setBranch(branch)
1145 self.assertBzrIdentity(branch, linked_branch.bzr_path)952 self.assertBzrIdentity(branch, linked_branch.bzr_path)
1146953
1147 def test_private_linked_to_product(self):954 def test_bzr_identity_private_linked_to_product(self):
1148 # Private branches also have a short lp:url.955 # Private branches also have a short lp:url.
1149 branch = self.factory.makeProductBranch(956 branch = self.factory.makeProductBranch(
1150 information_type=InformationType.USERDATA)957 information_type=InformationType.USERDATA)
@@ -1153,7 +960,7 @@
1153 ICanHasLinkedBranch(product).setBranch(branch)960 ICanHasLinkedBranch(product).setBranch(branch)
1154 self.assertBzrIdentity(branch, product.name)961 self.assertBzrIdentity(branch, product.name)
1155962
1156 def test_linked_to_series_and_dev_focus(self):963 def test_bzr_identity_linked_to_series_and_dev_focus(self):
1157 # If a branch is the development focus branch for a product and the964 # If a branch is the development focus branch for a product and the
1158 # branch for a series, the bzr identity will be the storter of the two965 # branch for a series, the bzr identity will be the storter of the two
1159 # URLs.966 # URLs.
@@ -1167,7 +974,7 @@
1167 series_link.setBranch(branch)974 series_link.setBranch(branch)
1168 self.assertBzrIdentity(branch, product_link.bzr_path)975 self.assertBzrIdentity(branch, product_link.bzr_path)
1169976
1170 def test_junk_branch_always_unique_name(self):977 def test_bzr_identity_junk_branch_always_unique_name(self):
1171 # For junk branches, the bzr identity is always based on the unique978 # For junk branches, the bzr identity is always based on the unique
1172 # name of the branch, even if it's linked to a product, product series979 # name of the branch, even if it's linked to a product, product series
1173 # or whatever.980 # or whatever.
@@ -1176,7 +983,7 @@
1176 ICanHasLinkedBranch(product).setBranch(branch)983 ICanHasLinkedBranch(product).setBranch(branch)
1177 self.assertBzrIdentity(branch, branch.unique_name)984 self.assertBzrIdentity(branch, branch.unique_name)
1178985
1179 def test_linked_to_package(self):986 def test_bzr_identity_linked_to_package(self):
1180 # If a branch is linked to a pocket of a package, then the987 # If a branch is linked to a pocket of a package, then the
1181 # bzr identity is the path to that package.988 # bzr identity is the path to that package.
1182 branch = self.factory.makePackageBranch()989 branch = self.factory.makePackageBranch()
@@ -1191,7 +998,7 @@
1191 login(ANONYMOUS)998 login(ANONYMOUS)
1192 self.assertBzrIdentity(branch, linked_branch.bzr_path)999 self.assertBzrIdentity(branch, linked_branch.bzr_path)
11931000
1194 def test_linked_to_dev_package(self):1001 def test_bzr_identity_linked_to_dev_package(self):
1195 # If a branch is linked to the development focus version of a package1002 # If a branch is linked to the development focus version of a package
1196 # then the bzr identity is distro/package.1003 # then the bzr identity is distro/package.
1197 sourcepackage = self.factory.makeSourcePackage()1004 sourcepackage = self.factory.makeSourcePackage()
@@ -1205,6 +1012,192 @@
1205 linked_branch.setBranch, branch, registrant)1012 linked_branch.setBranch, branch, registrant)
1206 self.assertBzrIdentity(branch, linked_branch.bzr_path)1013 self.assertBzrIdentity(branch, linked_branch.bzr_path)
12071014
1015 def test_identities_no_links(self):
1016 # If there are no links, the only branch identity is the unique name.
1017 branch = self.factory.makeAnyBranch()
1018 self.assertEqual(
1019 [(branch.unique_name, branch)], branch.getBranchIdentities())
1020
1021 def test_linked_to_product(self):
1022 # If a branch is linked to the product, it is also by definition
1023 # linked to the development focus of the product.
1024 fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
1025 fooix.development_focus.name = 'devel'
1026 eric = self.factory.makePerson(name='eric')
1027 branch = self.factory.makeProductBranch(
1028 product=fooix, owner=eric, name='trunk')
1029 linked_branch = ICanHasLinkedBranch(fooix)
1030 linked_branch.setBranch(branch)
1031 self.assertEqual(
1032 [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
1033 branch.getBranchLinks())
1034 self.assertEqual(
1035 [('fooix', fooix),
1036 ('fooix/devel', fooix.development_focus),
1037 ('~eric/fooix/trunk', branch)],
1038 branch.getBranchIdentities())
1039
1040 def test_linked_to_product_series(self):
1041 # If a branch is linked to a non-development series of a product and
1042 # not linked to the product itself, then only the product series is
1043 # returned in the links.
1044 fooix = removeSecurityProxy(self.factory.makeProduct(name='fooix'))
1045 future = self.factory.makeProductSeries(product=fooix, name='future')
1046 eric = self.factory.makePerson(name='eric')
1047 branch = self.factory.makeProductBranch(
1048 product=fooix, owner=eric, name='trunk')
1049 linked_branch = ICanHasLinkedBranch(future)
1050 login_person(fooix.owner)
1051 linked_branch.setBranch(branch)
1052 self.assertEqual(
1053 [linked_branch],
1054 branch.getBranchLinks())
1055 self.assertEqual(
1056 [('fooix/future', future),
1057 ('~eric/fooix/trunk', branch)],
1058 branch.getBranchIdentities())
1059
1060 def test_linked_to_package(self):
1061 # If a branch is linked to a suite source package where the
1062 # distroseries is the current series for the distribution, there is a
1063 # link for both the distribution source package and the suite source
1064 # package.
1065 mint = self.factory.makeDistribution(name='mint')
1066 dev = self.factory.makeDistroSeries(
1067 distribution=mint, version='1.0', name='dev')
1068 eric = self.factory.makePerson(name='eric')
1069 branch = self.factory.makePackageBranch(
1070 distroseries=dev, sourcepackagename='choc', name='tip',
1071 owner=eric)
1072 dsp = self.factory.makeDistributionSourcePackage('choc', mint)
1073 distro_link = ICanHasLinkedBranch(dsp)
1074 development_package = dsp.development_version
1075 suite_sourcepackage = development_package.getSuiteSourcePackage(
1076 PackagePublishingPocket.RELEASE)
1077 suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
1078
1079 registrant = suite_sourcepackage.distribution.owner
1080 run_with_login(
1081 registrant,
1082 suite_sp_link.setBranch, branch, registrant)
1083
1084 self.assertEqual(
1085 [distro_link, suite_sp_link],
1086 branch.getBranchLinks())
1087 self.assertEqual(
1088 [('mint/choc', dsp),
1089 ('mint/dev/choc', suite_sourcepackage),
1090 ('~eric/mint/dev/choc/tip', branch)],
1091 branch.getBranchIdentities())
1092
1093 def test_linked_to_package_not_release_pocket(self):
1094 # If a branch is linked to a suite source package where the
1095 # distroseries is the current series for the distribution, but the
1096 # pocket is not the RELEASE pocket, then there is only the link for
1097 # the suite source package.
1098 mint = self.factory.makeDistribution(name='mint')
1099 dev = self.factory.makeDistroSeries(
1100 distribution=mint, version='1.0', name='dev')
1101 eric = self.factory.makePerson(name='eric')
1102 branch = self.factory.makePackageBranch(
1103 distroseries=dev, sourcepackagename='choc', name='tip',
1104 owner=eric)
1105 dsp = self.factory.makeDistributionSourcePackage('choc', mint)
1106 development_package = dsp.development_version
1107 suite_sourcepackage = development_package.getSuiteSourcePackage(
1108 PackagePublishingPocket.BACKPORTS)
1109 suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
1110
1111 registrant = suite_sourcepackage.distribution.owner
1112 run_with_login(
1113 registrant,
1114 suite_sp_link.setBranch, branch, registrant)
1115
1116 self.assertEqual(
1117 [suite_sp_link],
1118 branch.getBranchLinks())
1119 self.assertEqual(
1120 [('mint/dev-backports/choc', suite_sourcepackage),
1121 ('~eric/mint/dev/choc/tip', branch)],
1122 branch.getBranchIdentities())
1123
1124 def test_linked_to_package_not_current_series(self):
1125 # If the branch is linked to a suite source package where the distro
1126 # series is not the current series, only the suite source package is
1127 # returned in the links.
1128 mint = self.factory.makeDistribution(name='mint')
1129 self.factory.makeDistroSeries(
1130 distribution=mint, version='1.0', name='dev')
1131 supported = self.factory.makeDistroSeries(
1132 distribution=mint, version='0.9', name='supported')
1133 eric = self.factory.makePerson(name='eric')
1134 branch = self.factory.makePackageBranch(
1135 distroseries=supported, sourcepackagename='choc', name='tip',
1136 owner=eric)
1137 suite_sp = self.factory.makeSuiteSourcePackage(
1138 distroseries=supported, sourcepackagename='choc',
1139 pocket=PackagePublishingPocket.RELEASE)
1140 suite_sp_link = ICanHasLinkedBranch(suite_sp)
1141
1142 registrant = suite_sp.distribution.owner
1143 run_with_login(
1144 registrant,
1145 suite_sp_link.setBranch, branch, registrant)
1146
1147 self.assertEqual(
1148 [suite_sp_link],
1149 branch.getBranchLinks())
1150 self.assertEqual(
1151 [('mint/supported/choc', suite_sp),
1152 ('~eric/mint/supported/choc/tip', branch)],
1153 branch.getBranchIdentities())
1154
1155 def test_linked_across_project_to_package(self):
1156 # If a product branch is linked to a suite source package, the links
1157 # are the same as if it was a source package branch.
1158 mint = self.factory.makeDistribution(name='mint')
1159 self.factory.makeDistroSeries(
1160 distribution=mint, version='1.0', name='dev')
1161 eric = self.factory.makePerson(name='eric')
1162 fooix = self.factory.makeProduct(name='fooix')
1163 branch = self.factory.makeProductBranch(
1164 product=fooix, owner=eric, name='trunk')
1165 dsp = self.factory.makeDistributionSourcePackage('choc', mint)
1166 distro_link = ICanHasLinkedBranch(dsp)
1167 development_package = dsp.development_version
1168 suite_sourcepackage = development_package.getSuiteSourcePackage(
1169 PackagePublishingPocket.RELEASE)
1170 suite_sp_link = ICanHasLinkedBranch(suite_sourcepackage)
1171
1172 registrant = suite_sourcepackage.distribution.owner
1173 run_with_login(
1174 registrant,
1175 suite_sp_link.setBranch, branch, registrant)
1176
1177 self.assertEqual(
1178 [distro_link, suite_sp_link],
1179 branch.getBranchLinks())
1180 self.assertEqual(
1181 [('mint/choc', dsp),
1182 ('mint/dev/choc', suite_sourcepackage),
1183 ('~eric/fooix/trunk', branch)],
1184 branch.getBranchIdentities())
1185
1186 def test_junk_branch_links(self):
1187 # If a junk branch has links, those links are returned by
1188 # getBranchLinks, but getBranchIdentities just returns the branch
1189 # unique name.
1190 eric = self.factory.makePerson(name='eric')
1191 branch = self.factory.makePersonalBranch(owner=eric, name='foo')
1192 fooix = removeSecurityProxy(self.factory.makeProduct())
1193 linked_branch = ICanHasLinkedBranch(fooix)
1194 linked_branch.setBranch(branch)
1195 self.assertEqual(
1196 [linked_branch, ICanHasLinkedBranch(fooix.development_focus)],
1197 branch.getBranchLinks())
1198 self.assertEqual(
1199 [('~eric/+junk/foo', branch)], branch.getBranchIdentities())
1200
12081201
1209class TestBranchDeletion(TestCaseWithFactory):1202class TestBranchDeletion(TestCaseWithFactory):
1210 """Test the different cases that makes a branch deletable or not."""1203 """Test the different cases that makes a branch deletable or not."""
@@ -1442,6 +1435,13 @@
1442 )1435 )
1443 self.branch.destroySelf(break_references=True)1436 self.branch.destroySelf(break_references=True)
14441437
1438 def test_related_webhooks_deleted(self):
1439 webhook = self.factory.makeWebhook(target=self.branch)
1440 webhook.ping()
1441 self.branch.destroySelf()
1442 transaction.commit()
1443 self.assertRaises(LostObjectError, getattr, webhook, 'target')
1444
14451445
1446class TestBranchDeletionConsequences(TestCase):1446class TestBranchDeletionConsequences(TestCase):
1447 """Test determination and application of branch deletion consequences."""1447 """Test determination and application of branch deletion consequences."""
14481448
=== modified file 'lib/lp/code/model/tests/test_branchlookup.py'
--- lib/lp/code/model/tests/test_branchlookup.py 2012-11-12 13:38:27 +0000
+++ lib/lp/code/model/tests/test_branchlookup.py 2015-09-29 15:54:57 +0000
@@ -153,7 +153,7 @@
153 self.assertEqual((branch, '/foo'), result)153 self.assertEqual((branch, '/foo'), result)
154154
155155
156class TestGetByPath(TestCaseWithFactory):156class TestGetByLPPath(TestCaseWithFactory):
157 """Test `IBranchLookup.getByLPPath`."""157 """Test `IBranchLookup.getByLPPath`."""
158158
159 layer = DatabaseFunctionalLayer159 layer = DatabaseFunctionalLayer
@@ -171,54 +171,51 @@
171 self.factory.getUniqueString()171 self.factory.getUniqueString()
172 for i in range(arbitrary_num_segments)])172 for i in range(arbitrary_num_segments)])
173173
174 def assertMissingPath(self, exctype, path):
175 self.assertRaises(exctype, self.getByPath, path)
176
177 def assertPath(self, expected_branch, expected_suffix, path):
178 branch, suffix = self.getByPath(path)
179 self.assertEqual(expected_branch, branch)
180 self.assertEqual(expected_suffix, suffix)
181
174 def test_finds_exact_personal_branch(self):182 def test_finds_exact_personal_branch(self):
175 branch = self.factory.makePersonalBranch()183 branch = self.factory.makePersonalBranch()
176 found_branch, suffix = self.getByPath(branch.unique_name)184 self.assertPath(branch, '', branch.unique_name)
177 self.assertEqual(branch, found_branch)
178 self.assertEqual('', suffix)
179185
180 def test_finds_suffixed_personal_branch(self):186 def test_finds_suffixed_personal_branch(self):
181 branch = self.factory.makePersonalBranch()187 branch = self.factory.makePersonalBranch()
182 suffix = self.makeRelativePath()188 suffix = self.makeRelativePath()
183 found_branch, found_suffix = self.getByPath(189 self.assertPath(branch, suffix, branch.unique_name + '/' + suffix)
184 branch.unique_name + '/' + suffix)
185 self.assertEqual(branch, found_branch)
186 self.assertEqual(suffix, found_suffix)
187190
188 def test_missing_personal_branch(self):191 def test_missing_personal_branch(self):
189 owner = self.factory.makePerson()192 owner = self.factory.makePerson()
190 namespace = get_branch_namespace(owner)193 namespace = get_branch_namespace(owner)
191 branch_name = namespace.getBranchName(self.factory.getUniqueString())194 branch_name = namespace.getBranchName(self.factory.getUniqueString())
192 self.assertRaises(NoSuchBranch, self.getByPath, branch_name)195 self.assertMissingPath(NoSuchBranch, branch_name)
193196
194 def test_missing_suffixed_personal_branch(self):197 def test_missing_suffixed_personal_branch(self):
195 owner = self.factory.makePerson()198 owner = self.factory.makePerson()
196 namespace = get_branch_namespace(owner)199 namespace = get_branch_namespace(owner)
197 branch_name = namespace.getBranchName(self.factory.getUniqueString())200 branch_name = namespace.getBranchName(self.factory.getUniqueString())
198 suffix = self.makeRelativePath()201 suffix = self.makeRelativePath()
199 self.assertRaises(202 self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix)
200 NoSuchBranch, self.getByPath, branch_name + '/' + suffix)
201203
202 def test_finds_exact_product_branch(self):204 def test_finds_exact_product_branch(self):
203 branch = self.factory.makeProductBranch()205 branch = self.factory.makeProductBranch()
204 found_branch, suffix = self.getByPath(branch.unique_name)206 self.assertPath(branch, '', branch.unique_name)
205 self.assertEqual(branch, found_branch)
206 self.assertEqual('', suffix)
207207
208 def test_finds_suffixed_product_branch(self):208 def test_finds_suffixed_product_branch(self):
209 branch = self.factory.makeProductBranch()209 branch = self.factory.makeProductBranch()
210 suffix = self.makeRelativePath()210 suffix = self.makeRelativePath()
211 found_branch, found_suffix = self.getByPath(211 self.assertPath(branch, suffix, branch.unique_name + '/' + suffix)
212 branch.unique_name + '/' + suffix)
213 self.assertEqual(branch, found_branch)
214 self.assertEqual(suffix, found_suffix)
215212
216 def test_missing_product_branch(self):213 def test_missing_product_branch(self):
217 owner = self.factory.makePerson()214 owner = self.factory.makePerson()
218 product = self.factory.makeProduct()215 product = self.factory.makeProduct()
219 namespace = get_branch_namespace(owner, product=product)216 namespace = get_branch_namespace(owner, product=product)
220 branch_name = namespace.getBranchName(self.factory.getUniqueString())217 branch_name = namespace.getBranchName(self.factory.getUniqueString())
221 self.assertRaises(NoSuchBranch, self.getByPath, branch_name)218 self.assertMissingPath(NoSuchBranch, branch_name)
222219
223 def test_missing_suffixed_product_branch(self):220 def test_missing_suffixed_product_branch(self):
224 owner = self.factory.makePerson()221 owner = self.factory.makePerson()
@@ -226,14 +223,11 @@
226 namespace = get_branch_namespace(owner, product=product)223 namespace = get_branch_namespace(owner, product=product)
227 suffix = self.makeRelativePath()224 suffix = self.makeRelativePath()
228 branch_name = namespace.getBranchName(self.factory.getUniqueString())225 branch_name = namespace.getBranchName(self.factory.getUniqueString())
229 self.assertRaises(226 self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix)
230 NoSuchBranch, self.getByPath, branch_name + '/' + suffix)
231227
232 def test_finds_exact_package_branch(self):228 def test_finds_exact_package_branch(self):
233 branch = self.factory.makePackageBranch()229 branch = self.factory.makePackageBranch()
234 found_branch, suffix = self.getByPath(branch.unique_name)230 self.assertPath(branch, '', branch.unique_name)
235 self.assertEqual(branch, found_branch)
236 self.assertEqual('', suffix)
237231
238 def test_missing_package_branch(self):232 def test_missing_package_branch(self):
239 owner = self.factory.makePerson()233 owner = self.factory.makePerson()
@@ -243,7 +237,7 @@
243 owner, distroseries=distroseries,237 owner, distroseries=distroseries,
244 sourcepackagename=sourcepackagename)238 sourcepackagename=sourcepackagename)
245 branch_name = namespace.getBranchName(self.factory.getUniqueString())239 branch_name = namespace.getBranchName(self.factory.getUniqueString())
246 self.assertRaises(NoSuchBranch, self.getByPath, branch_name)240 self.assertMissingPath(NoSuchBranch, branch_name)
247241
248 def test_missing_suffixed_package_branch(self):242 def test_missing_suffixed_package_branch(self):
249 owner = self.factory.makePerson()243 owner = self.factory.makePerson()
@@ -254,19 +248,30 @@
254 sourcepackagename=sourcepackagename)248 sourcepackagename=sourcepackagename)
255 suffix = self.makeRelativePath()249 suffix = self.makeRelativePath()
256 branch_name = namespace.getBranchName(self.factory.getUniqueString())250 branch_name = namespace.getBranchName(self.factory.getUniqueString())
257 self.assertRaises(251 self.assertMissingPath(NoSuchBranch, branch_name + '/' + suffix)
258 NoSuchBranch, self.getByPath, branch_name + '/' + suffix)
259252
260 def test_too_short(self):253 def test_too_short(self):
261 person = self.factory.makePerson()254 person = self.factory.makePerson()
262 self.assertRaises(255 self.assertMissingPath(InvalidNamespace, '~%s' % person.name)
263 InvalidNamespace, self.getByPath, '~%s' % person.name)
264256
265 def test_no_such_product(self):257 def test_no_such_product(self):
266 person = self.factory.makePerson()258 person = self.factory.makePerson()
267 branch_name = '~%s/%s/%s' % (259 branch_name = '~%s/%s/%s' % (
268 person.name, self.factory.getUniqueString(), 'branch-name')260 person.name, self.factory.getUniqueString(), 'branch-name')
269 self.assertRaises(NoSuchProduct, self.getByPath, branch_name)261 self.assertMissingPath(NoSuchProduct, branch_name)
262
263
264class TestGetByPath(TestGetByLPPath):
265 """Test `IBranchLookup.getByPath`."""
266
267 def getByPath(self, path):
268 return self.branch_lookup.getByPath(path)
269
270 def assertMissingPath(self, exctype, path):
271 self.assertIsNone(self.getByPath(path))
272
273 def assertPath(self, expected_branch, expected_suffix, path):
274 self.assertEqual(expected_branch, self.getByPath(path))
270275
271276
272class TestGetByUrl(TestCaseWithFactory):277class TestGetByUrl(TestCaseWithFactory):
273278
=== modified file 'lib/lp/code/model/tests/test_branchset.py'
--- lib/lp/code/model/tests/test_branchset.py 2012-09-18 18:36:09 +0000
+++ lib/lp/code/model/tests/test_branchset.py 2015-09-29 15:54:57 +0000
@@ -10,7 +10,9 @@
1010
11from lp.app.enums import InformationType11from lp.app.enums import InformationType
12from lp.code.interfaces.branch import IBranchSet12from lp.code.interfaces.branch import IBranchSet
13from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
13from lp.code.model.branch import BranchSet14from lp.code.model.branch import BranchSet
15from lp.services.propertycache import clear_property_cache
14from lp.testing import (16from lp.testing import (
15 login_person,17 login_person,
16 logout,18 logout,
@@ -46,6 +48,16 @@
46 branches = BranchSet().getByUrls([url])48 branches = BranchSet().getByUrls([url])
47 self.assertEqual({url: None}, branches)49 self.assertEqual({url: None}, branches)
4850
51 def test_getByPath(self):
52 branch = self.factory.makeProductBranch()
53 self.assertEqual(branch, BranchSet().getByPath(branch.shortened_path))
54 product = removeSecurityProxy(branch.product)
55 ICanHasLinkedBranch(product).setBranch(branch)
56 clear_property_cache(branch)
57 self.assertEqual(product.name, branch.shortened_path)
58 self.assertEqual(branch, BranchSet().getByPath(branch.shortened_path))
59 self.assertIsNone(BranchSet().getByPath('nonexistent'))
60
49 def test_api_branches_query_count(self):61 def test_api_branches_query_count(self):
50 webservice = LaunchpadWebServiceCaller()62 webservice = LaunchpadWebServiceCaller()
51 collector = QueryCollector()63 collector = QueryCollector()
5264
=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
--- lib/lp/code/model/tests/test_branchtarget.py 2014-11-28 22:07:05 +0000
+++ lib/lp/code/model/tests/test_branchtarget.py 2015-09-29 15:54:57 +0000
@@ -141,9 +141,9 @@
141 # Package branches do support merge proposals.141 # Package branches do support merge proposals.
142 self.assertTrue(self.target.supports_merge_proposals)142 self.assertTrue(self.target.supports_merge_proposals)
143143
144 def test_supports_short_identites(self):144 def test_supports_short_identities(self):
145 # Package branches do support short bzr identites.145 # Package branches do support short bzr identities.
146 self.assertTrue(self.target.supports_short_identites)146 self.assertTrue(self.target.supports_short_identities)
147147
148 def test_displayname(self):148 def test_displayname(self):
149 # The display name of a source package target is the display name of149 # The display name of a source package target is the display name of
@@ -280,9 +280,9 @@
280 # Personal branches do not support merge proposals.280 # Personal branches do not support merge proposals.
281 self.assertFalse(self.target.supports_merge_proposals)281 self.assertFalse(self.target.supports_merge_proposals)
282282
283 def test_supports_short_identites(self):283 def test_supports_short_identities(self):
284 # Personal branches do not support short bzr identites.284 # Personal branches do not support short bzr identities.
285 self.assertFalse(self.target.supports_short_identites)285 self.assertFalse(self.target.supports_short_identities)
286286
287 def test_displayname(self):287 def test_displayname(self):
288 # The display name of a person branch target is ~$USER/+junk.288 # The display name of a person branch target is ~$USER/+junk.
@@ -405,9 +405,9 @@
405 # Product branches do support merge proposals.405 # Product branches do support merge proposals.
406 self.assertTrue(self.target.supports_merge_proposals)406 self.assertTrue(self.target.supports_merge_proposals)
407407
408 def test_supports_short_identites(self):408 def test_supports_short_identities(self):
409 # Product branches do support short bzr identites.409 # Product branches do support short bzr identities.
410 self.assertTrue(self.target.supports_short_identites)410 self.assertTrue(self.target.supports_short_identities)
411411
412 def test_displayname(self):412 def test_displayname(self):
413 # The display name of a product branch target is the display name of413 # The display name of a product branch target is the display name of
414414
=== modified file 'lib/lp/codehosting/scanner/bzrsync.py'
--- lib/lp/codehosting/scanner/bzrsync.py 2015-02-25 12:48:27 +0000
+++ lib/lp/codehosting/scanner/bzrsync.py 2015-09-29 15:54:57 +0000
@@ -1,6 +1,6 @@
1#!/usr/bin/python1#!/usr/bin/python
2#2#
3# Copyright 2009-2010 Canonical Ltd. This software is licensed under the3# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6"""Import version control metadata from a Bazaar branch into the database."""6"""Import version control metadata from a Bazaar branch into the database."""
@@ -34,7 +34,9 @@
34from lp.code.model.revision import Revision34from lp.code.model.revision import Revision
35from lp.codehosting.scanner import events35from lp.codehosting.scanner import events
36from lp.services.config import config36from lp.services.config import config
37from lp.services.features import getFeatureFlag
37from lp.services.utils import iter_list_chunks38from lp.services.utils import iter_list_chunks
39from lp.services.webhooks.interfaces import IWebhookSet
38from lp.translations.interfaces.translationtemplatesbuild import (40from lp.translations.interfaces.translationtemplatesbuild import (
39 ITranslationTemplatesBuildSource,41 ITranslationTemplatesBuildSource,
40 )42 )
@@ -323,3 +325,13 @@
323325
324def update_recipes(tip_changed):326def update_recipes(tip_changed):
325 tip_changed.db_branch.markRecipesStale()327 tip_changed.db_branch.markRecipesStale()
328
329
330def trigger_webhooks(tip_changed):
331 old_revid = tip_changed.old_tip_revision_id
332 new_revid = tip_changed.new_tip_revision_id
333 if getFeatureFlag("code.bzr.webhooks.enabled") and old_revid != new_revid:
334 payload = tip_changed.composeWebhookPayload(
335 tip_changed.db_branch, old_revid, new_revid)
336 getUtility(IWebhookSet).trigger(
337 tip_changed.db_branch, "bzr:push:0.1", payload)
326338
=== modified file 'lib/lp/codehosting/scanner/events.py'
--- lib/lp/codehosting/scanner/events.py 2015-07-08 16:05:11 +0000
+++ lib/lp/codehosting/scanner/events.py 2015-09-29 15:54:57 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Events generated by the scanner."""4"""Events generated by the scanner."""
@@ -82,6 +82,14 @@
82 """The new tip revision id from this scan."""82 """The new tip revision id from this scan."""
83 return self.bzr_branch.last_revision()83 return self.bzr_branch.last_revision()
8484
85 @staticmethod
86 def composeWebhookPayload(branch, old_revid, new_revid):
87 return {
88 "bzr_branch_path": branch.shortened_path,
89 "old": {"revision_id": old_revid},
90 "new": {"revision_id": new_revid},
91 }
92
8593
86class IRevisionsRemoved(IObjectEvent):94class IRevisionsRemoved(IObjectEvent):
87 """Revisions have been removed from the branch."""95 """Revisions have been removed from the branch."""
8896
=== modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py'
--- lib/lp/codehosting/scanner/tests/test_bzrsync.py 2013-07-04 07:58:00 +0000
+++ lib/lp/codehosting/scanner/tests/test_bzrsync.py 2015-09-29 15:54:57 +0000
@@ -1,6 +1,6 @@
1#!/usr/bin/python1#!/usr/bin/python
2#2#
3# Copyright 2009-2012 Canonical Ltd. This software is licensed under the3# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6import datetime6import datetime
@@ -17,6 +17,11 @@
17from fixtures import TempDir17from fixtures import TempDir
18import pytz18import pytz
19from storm.locals import Store19from storm.locals import Store
20from testtools.matchers import (
21 Equals,
22 MatchesDict,
23 MatchesStructure,
24 )
20from twisted.python.util import mergeFunctionMetadata25from twisted.python.util import mergeFunctionMetadata
21from zope.component import getUtility26from zope.component import getUtility
22from zope.security.proxy import removeSecurityProxy27from zope.security.proxy import removeSecurityProxy
@@ -44,6 +49,7 @@
44from lp.codehosting.scanner.bzrsync import BzrSync49from lp.codehosting.scanner.bzrsync import BzrSync
45from lp.services.config import config50from lp.services.config import config
46from lp.services.database.interfaces import IStore51from lp.services.database.interfaces import IStore
52from lp.services.features.testing import FeatureFixture
47from lp.services.osutils import override_environ53from lp.services.osutils import override_environ
48from lp.testing import TestCaseWithFactory54from lp.testing import TestCaseWithFactory
49from lp.testing.dbuser import (55from lp.testing.dbuser import (
@@ -743,6 +749,32 @@
743 self.assertEqual(False, recipe.is_stale)749 self.assertEqual(False, recipe.is_stale)
744750
745751
752class TestTriggerWebhooks(BzrSyncTestCase):
753 """Test triggering of webhooks."""
754
755 def test_triggers_webhooks(self):
756 # On tip change, any relevant webhooks are triggered.
757 self.useFixture(FeatureFixture({"code.bzr.webhooks.enabled": "on"}))
758 self.syncAndCount()
759 old_revid = self.db_branch.last_scanned_id
760 with dbuser(config.launchpad.dbuser):
761 hook = self.factory.makeWebhook(
762 target=self.db_branch, event_types=["bzr:push:0.1"])
763 self.commitRevision()
764 new_revid = self.bzr_branch.last_revision()
765 self.makeBzrSync(self.db_branch).syncBranchAndClose()
766 delivery = hook.deliveries.one()
767 self.assertThat(
768 delivery,
769 MatchesStructure(
770 event_type=Equals("bzr:push:0.1"),
771 payload=MatchesDict({
772 "bzr_branch_path": Equals(self.db_branch.shortened_path),
773 "old": Equals({"revision_id": old_revid}),
774 "new": Equals({"revision_id": new_revid}),
775 })))
776
777
746class TestRevisionProperty(BzrSyncTestCase):778class TestRevisionProperty(BzrSyncTestCase):
747 """Tests for storting revision properties."""779 """Tests for storting revision properties."""
748780
749781
=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py 2015-07-15 03:58:56 +0000
+++ lib/lp/registry/browser/productseries.py 2015-09-29 15:54:57 +0000
@@ -417,7 +417,8 @@
417 @property417 @property
418 def long_bzr_identity(self):418 def long_bzr_identity(self):
419 """The bzr identity of the branch including the unique_name."""419 """The bzr identity of the branch including the unique_name."""
420 return self.context.branch.branchIdentities()[-1][0]420 lp_prefix = config.codehosting.bzr_lp_prefix
421 return lp_prefix + self.context.branch.getBranchIdentities()[-1][0]
421422
422 @property423 @property
423 def is_obsolete(self):424 def is_obsolete(self):
424425
=== modified file 'lib/lp/services/webhooks/client.py'
--- lib/lp/services/webhooks/client.py 2015-09-01 06:03:55 +0000
+++ lib/lp/services/webhooks/client.py 2015-09-29 15:54:57 +0000
@@ -1,7 +1,7 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Communication with the Git hosting service."""4"""Communication with webhook delivery endpoints."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
88
=== modified file 'lib/lp/services/webhooks/interfaces.py'
--- lib/lp/services/webhooks/interfaces.py 2015-09-09 06:11:43 +0000
+++ lib/lp/services/webhooks/interfaces.py 2015-09-29 15:54:57 +0000
@@ -71,13 +71,14 @@
7171
7272
73WEBHOOK_EVENT_TYPES = {73WEBHOOK_EVENT_TYPES = {
74 "bzr:push:0.1": "Bazaar push",
74 "git:push:0.1": "Git push",75 "git:push:0.1": "Git push",
75 }76 }
7677
7778
78@error_status(httplib.UNAUTHORIZED)79@error_status(httplib.UNAUTHORIZED)
79class WebhookFeatureDisabled(Exception):80class WebhookFeatureDisabled(Exception):
80 """Only certain users can create new Git repositories."""81 """Only certain users can create new webhooks."""
8182
82 def __init__(self):83 def __init__(self):
83 Exception.__init__(84 Exception.__init__(
8485
=== modified file 'lib/lp/services/webhooks/model.py'
--- lib/lp/services/webhooks/model.py 2015-09-09 06:11:43 +0000
+++ lib/lp/services/webhooks/model.py 2015-09-29 15:54:57 +0000
@@ -93,6 +93,9 @@
93 git_repository_id = Int(name='git_repository')93 git_repository_id = Int(name='git_repository')
94 git_repository = Reference(git_repository_id, 'GitRepository.id')94 git_repository = Reference(git_repository_id, 'GitRepository.id')
9595
96 branch_id = Int(name='branch')
97 branch = Reference(branch_id, 'Branch.id')
98
96 registrant_id = Int(name='registrant', allow_none=False)99 registrant_id = Int(name='registrant', allow_none=False)
97 registrant = Reference(registrant_id, 'Person.id')100 registrant = Reference(registrant_id, 'Person.id')
98 date_created = DateTime(tzinfo=utc, allow_none=False)101 date_created = DateTime(tzinfo=utc, allow_none=False)
@@ -108,6 +111,8 @@
108 def target(self):111 def target(self):
109 if self.git_repository is not None:112 if self.git_repository is not None:
110 return self.git_repository113 return self.git_repository
114 elif self.branch is not None:
115 return self.branch
111 else:116 else:
112 raise AssertionError("No target.")117 raise AssertionError("No target.")
113118
@@ -159,10 +164,13 @@
159164
160 def new(self, target, registrant, delivery_url, event_types, active,165 def new(self, target, registrant, delivery_url, event_types, active,
161 secret):166 secret):
167 from lp.code.interfaces.branch import IBranch
162 from lp.code.interfaces.gitrepository import IGitRepository168 from lp.code.interfaces.gitrepository import IGitRepository
163 hook = Webhook()169 hook = Webhook()
164 if IGitRepository.providedBy(target):170 if IGitRepository.providedBy(target):
165 hook.git_repository = target171 hook.git_repository = target
172 elif IBranch.providedBy(target):
173 hook.branch = target
166 else:174 else:
167 raise AssertionError("Unsupported target: %r" % (target,))175 raise AssertionError("Unsupported target: %r" % (target,))
168 hook.registrant = registrant176 hook.registrant = registrant
@@ -184,9 +192,12 @@
184 return IStore(Webhook).get(Webhook, id)192 return IStore(Webhook).get(Webhook, id)
185193
186 def findByTarget(self, target):194 def findByTarget(self, target):
195 from lp.code.interfaces.branch import IBranch
187 from lp.code.interfaces.gitrepository import IGitRepository196 from lp.code.interfaces.gitrepository import IGitRepository
188 if IGitRepository.providedBy(target):197 if IGitRepository.providedBy(target):
189 target_filter = Webhook.git_repository == target198 target_filter = Webhook.git_repository == target
199 elif IBranch.providedBy(target):
200 target_filter = Webhook.branch == target
190 else:201 else:
191 raise AssertionError("Unsupported target: %r" % (target,))202 raise AssertionError("Unsupported target: %r" % (target,))
192 return IStore(Webhook).find(Webhook, target_filter).order_by(203 return IStore(Webhook).find(Webhook, target_filter).order_by(
193204
=== modified file 'lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt'
--- lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-08-06 00:46:39 +0000
+++ lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-09-29 15:54:57 +0000
@@ -18,8 +18,8 @@
18 <div>18 <div>
19 <div class="beta" style="display: inline">19 <div class="beta" style="display: inline">
20 <img class="beta" alt="[BETA]" src="/@@/beta" /></div>20 <img class="beta" alt="[BETA]" src="/@@/beta" /></div>
21 The only currently supported events are Git pushes. We'll be21 The only currently supported events are Git and Bazaar pushes. We'll
22 rolling out webhooks for more soon.22 be rolling out webhooks for more soon.
23 </div>23 </div>
24 <ul class="horizontal">24 <ul class="horizontal">
25 <li>25 <li>
2626
=== modified file 'lib/lp/services/webhooks/tests/test_browser.py'
--- lib/lp/services/webhooks/tests/test_browser.py 2015-08-10 05:56:25 +0000
+++ lib/lp/services/webhooks/tests/test_browser.py 2015-09-29 15:54:57 +0000
@@ -27,6 +27,7 @@
27from lp.testing.matchers import HasQueryCount27from lp.testing.matchers import HasQueryCount
28from lp.testing.views import create_view28from lp.testing.views import create_view
2929
30
30breadcrumbs_tag = soupmatchers.Tag(31breadcrumbs_tag = soupmatchers.Tag(
31 'breadcrumbs', 'ol', attrs={'class': 'breadcrumbs'})32 'breadcrumbs', 'ol', attrs={'class': 'breadcrumbs'})
32webhooks_page_crumb_tag = soupmatchers.Tag(33webhooks_page_crumb_tag = soupmatchers.Tag(
@@ -47,12 +48,28 @@
47 'batch nav links', 'td', attrs={'class': 'batch-navigation-links'})48 'batch nav links', 'td', attrs={'class': 'batch-navigation-links'})
4849
4950
51class GitRepositoryTestHelpers:
52
53 event_type = "git:push:0.1"
54
55 def makeTarget(self):
56 return self.factory.makeGitRepository()
57
58
59class BranchTestHelpers:
60
61 event_type = "bzr:push:0.1"
62
63 def makeTarget(self):
64 return self.factory.makeBranch()
65
66
50class WebhookTargetViewTestHelpers:67class WebhookTargetViewTestHelpers:
5168
52 def setUp(self):69 def setUp(self):
53 super(WebhookTargetViewTestHelpers, self).setUp()70 super(WebhookTargetViewTestHelpers, self).setUp()
54 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))71 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
55 self.target = self.factory.makeGitRepository()72 self.target = self.makeTarget()
56 self.owner = self.target.owner73 self.owner = self.target.owner
57 login_person(self.owner)74 login_person(self.owner)
5875
@@ -65,7 +82,7 @@
65 return view82 return view
6683
6784
68class TestWebhooksView(WebhookTargetViewTestHelpers, TestCaseWithFactory):85class TestWebhooksViewBase(WebhookTargetViewTestHelpers):
6986
70 layer = DatabaseFunctionalLayer87 layer = DatabaseFunctionalLayer
7188
@@ -125,7 +142,19 @@
125 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))142 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
126143
127144
128class TestWebhookAddView(WebhookTargetViewTestHelpers, TestCaseWithFactory):145class TestWebhooksViewGitRepository(
146 TestWebhooksViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
147
148 pass
149
150
151class TestWebhooksViewBranch(
152 TestWebhooksViewBase, BranchTestHelpers, TestCaseWithFactory):
153
154 pass
155
156
157class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
129158
130 layer = DatabaseFunctionalLayer159 layer = DatabaseFunctionalLayer
131160
@@ -150,7 +179,7 @@
150 form={179 form={
151 "field.delivery_url": "http://example.com/test",180 "field.delivery_url": "http://example.com/test",
152 "field.active": "on", "field.event_types-empty-marker": "1",181 "field.active": "on", "field.event_types-empty-marker": "1",
153 "field.event_types": "git:push:0.1",182 "field.event_types": self.event_type,
154 "field.actions.new": "Add webhook"})183 "field.actions.new": "Add webhook"})
155 self.assertEqual([], view.errors)184 self.assertEqual([], view.errors)
156 hook = self.target.webhooks.one()185 hook = self.target.webhooks.one()
@@ -161,7 +190,7 @@
161 registrant=self.owner,190 registrant=self.owner,
162 delivery_url="http://example.com/test",191 delivery_url="http://example.com/test",
163 active=True,192 active=True,
164 event_types=["git:push:0.1"]))193 event_types=[self.event_type]))
165194
166 def test_rejects_bad_scheme(self):195 def test_rejects_bad_scheme(self):
167 transaction.commit()196 transaction.commit()
@@ -176,12 +205,24 @@
176 self.assertIs(None, self.target.webhooks.one())205 self.assertIs(None, self.target.webhooks.one())
177206
178207
208class TestWebhookAddViewGitRepository(
209 TestWebhookAddViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
210
211 pass
212
213
214class TestWebhookAddViewBranch(
215 TestWebhookAddViewBase, BranchTestHelpers, TestCaseWithFactory):
216
217 pass
218
219
179class WebhookViewTestHelpers:220class WebhookViewTestHelpers:
180221
181 def setUp(self):222 def setUp(self):
182 super(WebhookViewTestHelpers, self).setUp()223 super(WebhookViewTestHelpers, self).setUp()
183 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))224 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
184 self.target = self.factory.makeGitRepository()225 self.target = self.makeTarget()
185 self.owner = self.target.owner226 self.owner = self.target.owner
186 self.webhook = self.factory.makeWebhook(227 self.webhook = self.factory.makeWebhook(
187 target=self.target, delivery_url=u'http://example.com/original')228 target=self.target, delivery_url=u'http://example.com/original')
@@ -196,7 +237,7 @@
196 return view237 return view
197238
198239
199class TestWebhookView(WebhookViewTestHelpers, TestCaseWithFactory):240class TestWebhookViewBase(WebhookViewTestHelpers):
200241
201 layer = DatabaseFunctionalLayer242 layer = DatabaseFunctionalLayer
202243
@@ -249,7 +290,19 @@
249 event_types=[]))290 event_types=[]))
250291
251292
252class TestWebhookDeleteView(WebhookViewTestHelpers, TestCaseWithFactory):293class TestWebhookViewGitRepository(
294 TestWebhookViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
295
296 pass
297
298
299class TestWebhookViewBranch(
300 TestWebhookViewBase, BranchTestHelpers, TestCaseWithFactory):
301
302 pass
303
304
305class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
253306
254 layer = DatabaseFunctionalLayer307 layer = DatabaseFunctionalLayer
255308
@@ -281,3 +334,15 @@
281 form={"field.actions.delete": "Delete webhook"})334 form={"field.actions.delete": "Delete webhook"})
282 self.assertEqual([], view.errors)335 self.assertEqual([], view.errors)
283 self.assertIs(None, self.target.webhooks.one())336 self.assertIs(None, self.target.webhooks.one())
337
338
339class TestWebhookDeleteViewGitRepository(
340 TestWebhookDeleteViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
341
342 pass
343
344
345class TestWebhookDeleteViewBranch(
346 TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory):
347
348 pass
284349
=== modified file 'lib/lp/services/webhooks/tests/test_model.py'
--- lib/lp/services/webhooks/tests/test_model.py 2015-08-10 07:36:52 +0000
+++ lib/lp/services/webhooks/tests/test_model.py 2015-09-29 15:54:57 +0000
@@ -115,17 +115,17 @@
115 expected_set_permissions, checker.set_permissions, 'set')115 expected_set_permissions, checker.set_permissions, 'set')
116116
117117
118class TestWebhookSet(TestCaseWithFactory):118class TestWebhookSetBase:
119119
120 layer = DatabaseFunctionalLayer120 layer = DatabaseFunctionalLayer
121121
122 def test_new(self):122 def test_new(self):
123 target = self.factory.makeGitRepository()123 target = self.makeTarget()
124 login_person(target.owner)124 login_person(target.owner)
125 person = self.factory.makePerson()125 person = self.factory.makePerson()
126 hook = getUtility(IWebhookSet).new(126 hook = getUtility(IWebhookSet).new(
127 target, person, u'http://path/to/something', ['git:push'], True,127 target, person, u'http://path/to/something', [self.event_type],
128 u'sekrit')128 True, u'sekrit')
129 Store.of(hook).flush()129 Store.of(hook).flush()
130 self.assertEqual(target, hook.target)130 self.assertEqual(target, hook.target)
131 self.assertEqual(person, hook.registrant)131 self.assertEqual(person, hook.registrant)
@@ -134,7 +134,7 @@
134 self.assertEqual(u'http://path/to/something', hook.delivery_url)134 self.assertEqual(u'http://path/to/something', hook.delivery_url)
135 self.assertEqual(True, hook.active)135 self.assertEqual(True, hook.active)
136 self.assertEqual(u'sekrit', hook.secret)136 self.assertEqual(u'sekrit', hook.secret)
137 self.assertEqual(['git:push'], hook.event_types)137 self.assertEqual([self.event_type], hook.event_types)
138138
139 def test_getByID(self):139 def test_getByID(self):
140 hook1 = self.factory.makeWebhook()140 hook1 = self.factory.makeWebhook()
@@ -148,8 +148,8 @@
148 None, getUtility(IWebhookSet).getByID(1234))148 None, getUtility(IWebhookSet).getByID(1234))
149149
150 def test_findByTarget(self):150 def test_findByTarget(self):
151 target1 = self.factory.makeGitRepository()151 target1 = self.makeTarget()
152 target2 = self.factory.makeGitRepository()152 target2 = self.makeTarget()
153 for target, name in ((target1, 'one'), (target2, 'two')):153 for target, name in ((target1, 'one'), (target2, 'two')):
154 for i in range(3):154 for i in range(3):
155 self.factory.makeWebhook(155 self.factory.makeWebhook(
@@ -168,7 +168,7 @@
168 getUtility(IWebhookSet).findByTarget(target2)])168 getUtility(IWebhookSet).findByTarget(target2)])
169169
170 def test_delete(self):170 def test_delete(self):
171 target = self.factory.makeGitRepository()171 target = self.makeTarget()
172 login_person(target.owner)172 login_person(target.owner)
173 hooks = []173 hooks = []
174 for i in range(3):174 for i in range(3):
@@ -195,21 +195,21 @@
195195
196 def test_trigger(self):196 def test_trigger(self):
197 owner = self.factory.makePerson()197 owner = self.factory.makePerson()
198 target1 = self.factory.makeGitRepository(owner=owner)198 target1 = self.makeTarget(owner=owner)
199 target2 = self.factory.makeGitRepository(owner=owner)199 target2 = self.makeTarget(owner=owner)
200 hook1a = self.factory.makeWebhook(200 hook1a = self.factory.makeWebhook(
201 target=target1, event_types=[])201 target=target1, event_types=[])
202 hook1b = self.factory.makeWebhook(202 hook1b = self.factory.makeWebhook(
203 target=target1, event_types=['git:push:0.1'])203 target=target1, event_types=[self.event_type])
204 hook2a = self.factory.makeWebhook(204 hook2a = self.factory.makeWebhook(
205 target=target2, event_types=['git:push:0.1'])205 target=target2, event_types=[self.event_type])
206 hook2b = self.factory.makeWebhook(206 hook2b = self.factory.makeWebhook(
207 target=target2, event_types=['git:push:0.1'], active=False)207 target=target2, event_types=[self.event_type], active=False)
208208
209 # Only webhooks subscribed to the relevant target and event type209 # Only webhooks subscribed to the relevant target and event type
210 # are triggered.210 # are triggered.
211 getUtility(IWebhookSet).trigger(211 getUtility(IWebhookSet).trigger(
212 target1, 'git:push:0.1', {'some': 'payload'})212 target1, self.event_type, {'some': 'payload'})
213 with admin_logged_in():213 with admin_logged_in():
214 self.assertThat(list(hook1a.deliveries), HasLength(0))214 self.assertThat(list(hook1a.deliveries), HasLength(0))
215 self.assertThat(list(hook1b.deliveries), HasLength(1))215 self.assertThat(list(hook1b.deliveries), HasLength(1))
@@ -220,7 +220,7 @@
220220
221 # Disabled webhooks aren't triggered.221 # Disabled webhooks aren't triggered.
222 getUtility(IWebhookSet).trigger(222 getUtility(IWebhookSet).trigger(
223 target2, 'git:push:0.1', {'other': 'payload'})223 target2, self.event_type, {'other': 'payload'})
224 with admin_logged_in():224 with admin_logged_in():
225 self.assertThat(list(hook1a.deliveries), HasLength(0))225 self.assertThat(list(hook1a.deliveries), HasLength(0))
226 self.assertThat(list(hook1b.deliveries), HasLength(1))226 self.assertThat(list(hook1b.deliveries), HasLength(1))
@@ -228,3 +228,19 @@
228 self.assertThat(list(hook2b.deliveries), HasLength(0))228 self.assertThat(list(hook2b.deliveries), HasLength(0))
229 delivery = hook2a.deliveries.one()229 delivery = hook2a.deliveries.one()
230 self.assertEqual(delivery.payload, {'other': 'payload'})230 self.assertEqual(delivery.payload, {'other': 'payload'})
231
232
233class TestWebhookSetGitRepository(TestWebhookSetBase, TestCaseWithFactory):
234
235 event_type = 'git:push:0.1'
236
237 def makeTarget(self, owner=None):
238 return self.factory.makeGitRepository(owner=owner)
239
240
241class TestWebhookSetBranch(TestWebhookSetBase, TestCaseWithFactory):
242
243 event_type = 'bzr:push:0.1'
244
245 def makeTarget(self, owner=None):
246 return self.factory.makeBranch(owner=owner)
231247
=== modified file 'lib/lp/services/webhooks/tests/test_webservice.py'
--- lib/lp/services/webhooks/tests/test_webservice.py 2015-09-09 06:11:43 +0000
+++ lib/lp/services/webhooks/tests/test_webservice.py 2015-09-29 15:54:57 +0000
@@ -260,12 +260,12 @@
260 self.assertIs(None, representation['date_first_sent'])260 self.assertIs(None, representation['date_first_sent'])
261261
262262
263class TestWebhookTarget(TestCaseWithFactory):263class TestWebhookTargetBase:
264 layer = DatabaseFunctionalLayer264 layer = DatabaseFunctionalLayer
265265
266 def setUp(self):266 def setUp(self):
267 super(TestWebhookTarget, self).setUp()267 super(TestWebhookTargetBase, self).setUp()
268 self.target = self.factory.makeGitRepository()268 self.target = self.makeTarget()
269 self.owner = self.target.owner269 self.owner = self.target.owner
270 self.target_url = api_url(self.target)270 self.target_url = api_url(self.target)
271 self.webservice = webservice_for_person(271 self.webservice = webservice_for_person(
@@ -309,13 +309,13 @@
309 response = self.webservice.named_post(309 response = self.webservice.named_post(
310 self.target_url, 'newWebhook',310 self.target_url, 'newWebhook',
311 delivery_url='http://example.com/ep',311 delivery_url='http://example.com/ep',
312 event_types=['git:push:0.1'], api_version='devel')312 event_types=[self.event_type], api_version='devel')
313 self.assertEqual(201, response.status)313 self.assertEqual(201, response.status)
314314
315 representation = self.webservice.get(315 representation = self.webservice.get(
316 self.target_url + '/webhooks', api_version='devel').jsonBody()316 self.target_url + '/webhooks', api_version='devel').jsonBody()
317 self.assertContentEqual(317 self.assertContentEqual(
318 [('http://example.com/ep', ['git:push:0.1'], True)],318 [('http://example.com/ep', [self.event_type], True)],
319 [(entry['delivery_url'], entry['event_types'], entry['active'])319 [(entry['delivery_url'], entry['event_types'], entry['active'])
320 for entry in representation['entries']])320 for entry in representation['entries']])
321321
@@ -323,8 +323,9 @@
323 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))323 self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
324 response = self.webservice.named_post(324 response = self.webservice.named_post(
325 self.target_url, 'newWebhook',325 self.target_url, 'newWebhook',
326 delivery_url='http://example.com/ep', event_types=['git:push:0.1'],326 delivery_url='http://example.com/ep',
327 secret='sekrit', api_version='devel')327 event_types=[self.event_type], secret='sekrit',
328 api_version='devel')
328 self.assertEqual(201, response.status)329 self.assertEqual(201, response.status)
329330
330 # The secret is set, but cannot be read back through the API.331 # The secret is set, but cannot be read back through the API.
@@ -339,16 +340,33 @@
339 webservice = LaunchpadWebServiceCaller()340 webservice = LaunchpadWebServiceCaller()
340 response = webservice.named_post(341 response = webservice.named_post(
341 self.target_url, 'newWebhook',342 self.target_url, 'newWebhook',
342 delivery_url='http://example.com/ep', event_types=['git:push:0.1'],343 delivery_url='http://example.com/ep',
343 api_version='devel')344 event_types=[self.event_type], api_version='devel')
344 self.assertEqual(401, response.status)345 self.assertEqual(401, response.status)
345 self.assertIn('launchpad.Edit', response.body)346 self.assertIn('launchpad.Edit', response.body)
346347
347 def test_newWebhook_feature_flag_guard(self):348 def test_newWebhook_feature_flag_guard(self):
348 response = self.webservice.named_post(349 response = self.webservice.named_post(
349 self.target_url, 'newWebhook',350 self.target_url, 'newWebhook',
350 delivery_url='http://example.com/ep', event_types=['git:push:0.1'],351 delivery_url='http://example.com/ep',
351 api_version='devel')352 event_types=[self.event_type], api_version='devel')
352 self.assertEqual(401, response.status)353 self.assertEqual(401, response.status)
353 self.assertEqual(354 self.assertEqual(
354 'This webhook feature is not available yet.', response.body)355 'This webhook feature is not available yet.', response.body)
356
357
358class TestWebhookTargetGitRepository(
359 TestWebhookTargetBase, TestCaseWithFactory):
360
361 event_type = 'git:push:0.1'
362
363 def makeTarget(self):
364 return self.factory.makeGitRepository()
365
366
367class TestWebhookTargetBranch(TestWebhookTargetBase, TestCaseWithFactory):
368
369 event_type = 'bzr:push:0.1'
370
371 def makeTarget(self):
372 return self.factory.makeBranch()