← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/bzr-webhooks into lp:launchpad with lp:~cjwatson/launchpad/db-bzr-webhooks as a prerequisite.

Commit message:
Add webhook support for Bazaar branches.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #342729 in Launchpad itself: "Should support post-commit webhooks"
  https://bugs.launchpad.net/launchpad/+bug/342729

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/bzr-webhooks/+merge/272248

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.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/bzr-webhooks into lp:launchpad.
=== 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-24 13:55:55 +0000
@@ -125,6 +125,7 @@
 from lp.services import searchbuilder
 from lp.services.config import config
 from lp.services.database.constants import UTC_NOW
+from lp.services.features import getFeatureFlag
 from lp.services.feeds.browser import (
     BranchFeedLink,
     FeedsMixin,
@@ -152,6 +153,7 @@
 from lp.services.webapp.breadcrumb import NameBreadcrumb
 from lp.services.webapp.escaping import structured
 from lp.services.webapp.interfaces import ICanonicalUrlData
+from lp.services.webhooks.browser import WebhookTargetNavigationMixin
 from lp.snappy.browser.hassnaps import (
     HasSnapsMenuMixin,
     HasSnapsViewMixin,
@@ -183,7 +185,7 @@
         return self.context.target.components[-1]
 
 
-class BranchNavigation(Navigation):
+class BranchNavigation(WebhookTargetNavigationMixin, Navigation):
 
     usedfor = IBranch
 
@@ -240,7 +242,7 @@
     facet = 'branches'
     title = 'Edit branch'
     links = (
-        'edit', 'reviewer', 'edit_whiteboard', 'delete')
+        'edit', 'reviewer', 'edit_whiteboard', 'webhooks', 'delete')
 
     def branch_is_import(self):
         return self.context.branch_type == BranchType.IMPORTED
@@ -267,6 +269,13 @@
         text = 'Set branch reviewer'
         return Link('+reviewer', text, icon='edit')
 
+    @enabled_with_permission('launchpad.Edit')
+    def webhooks(self):
+        text = 'Manage webhooks'
+        return Link(
+            '+webhooks', text, icon='edit',
+            enabled=bool(getFeatureFlag('webhooks.new.enabled')))
+
 
 class BranchContextMenu(ContextMenu, HasRecipesMenuMixin, HasSnapsMenuMixin):
     """Context menu for branches."""

=== 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-24 13:55:55 +0000
@@ -418,6 +418,9 @@
       for="lp.codehosting.scanner.events.ITipChanged"
       handler="lp.codehosting.scanner.bzrsync.update_recipes"/>
   <subscriber
+      for="lp.codehosting.scanner.events.ITipChanged"
+      handler="lp.codehosting.scanner.bzrsync.trigger_webhooks"/>
+  <subscriber
       for="lp.codehosting.scanner.events.IRevisionsRemoved"
       handler="lp.codehosting.scanner.email.send_removed_revision_emails"/>
   <subscriber

=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py	2015-09-23 15:38:13 +0000
+++ lib/lp/code/interfaces/branch.py	2015-09-24 13:55:55 +0000
@@ -105,6 +105,7 @@
     structured,
     )
 from lp.services.webapp.interfaces import ITableBatchNavigator
+from lp.services.webhooks.interfaces import IWebhookTarget
 
 
 DEFAULT_BRANCH_STATUS_IN_LISTING = (
@@ -1125,7 +1126,7 @@
             vocabulary=ControlFormat))
 
 
-class IBranchEdit(Interface):
+class IBranchEdit(IWebhookTarget):
     """IBranch attributes that require launchpad.Edit permission."""
 
     @call_with(user=REQUEST_USER)

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2015-09-23 15:38:13 +0000
+++ lib/lp/code/model/branch.py	2015-09-24 13:55:55 +0000
@@ -179,10 +179,12 @@
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp import urlappend
 from lp.services.webapp.authorization import check_permission
+from lp.services.webhooks.interfaces import IWebhookSet
+from lp.services.webhooks.model import WebhookTargetMixin
 
 
 @implementer(IBranch, IPrivacy, IInformationType)
-class Branch(SQLBase, BzrIdentityMixin):
+class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
     """A sequence of ordered revisions in Bazaar."""
     _table = 'Branch'
 
@@ -1331,6 +1333,7 @@
 
         self._deleteBranchSubscriptions()
         self._deleteJobs()
+        getUtility(IWebhookSet).delete(self.webhooks)
 
         # Now destroy the branch.
         branch_id = self.id

=== 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-24 13:55:55 +0000
@@ -1442,6 +1442,13 @@
         )
         self.branch.destroySelf(break_references=True)
 
+    def test_related_webhooks_deleted(self):
+        webhook = self.factory.makeWebhook(target=self.branch)
+        webhook.ping()
+        self.branch.destroySelf()
+        transaction.commit()
+        self.assertRaises(LostObjectError, getattr, webhook, 'target')
+
 
 class TestBranchDeletionConsequences(TestCase):
     """Test determination and application of branch deletion consequences."""

=== 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-24 13:55:55 +0000
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 #
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Import version control metadata from a Bazaar branch into the database."""
@@ -34,7 +34,9 @@
 from lp.code.model.revision import Revision
 from lp.codehosting.scanner import events
 from lp.services.config import config
+from lp.services.features import getFeatureFlag
 from lp.services.utils import iter_list_chunks
+from lp.services.webhooks.interfaces import IWebhookSet
 from lp.translations.interfaces.translationtemplatesbuild import (
     ITranslationTemplatesBuildSource,
     )
@@ -323,3 +325,13 @@
 
 def update_recipes(tip_changed):
     tip_changed.db_branch.markRecipesStale()
+
+
+def trigger_webhooks(tip_changed):
+    old_revid = tip_changed.old_tip_revision_id
+    new_revid = tip_changed.new_tip_revision_id
+    if getFeatureFlag("code.bzr.webhooks.enabled") and old_revid != new_revid:
+        payload = tip_changed.composeWebhookPayload(
+            tip_changed.db_branch, old_revid, new_revid)
+        getUtility(IWebhookSet).trigger(
+            tip_changed.db_branch, "bzr:push:0.1", payload)

=== 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-24 13:55:55 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Events generated by the scanner."""
@@ -82,6 +82,14 @@
         """The new tip revision id from this scan."""
         return self.bzr_branch.last_revision()
 
+    @staticmethod
+    def composeWebhookPayload(branch, old_revid, new_revid):
+        return {
+            "bzr_branch_path": branch.unique_name,
+            "old": {"revision_id": old_revid},
+            "new": {"revision_id": new_revid},
+            }
+
 
 class IRevisionsRemoved(IObjectEvent):
     """Revisions have been removed from the branch."""

=== 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-24 13:55:55 +0000
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 #
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import datetime
@@ -17,6 +17,11 @@
 from fixtures import TempDir
 import pytz
 from storm.locals import Store
+from testtools.matchers import (
+    Equals,
+    MatchesDict,
+    MatchesStructure,
+    )
 from twisted.python.util import mergeFunctionMetadata
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -44,6 +49,7 @@
 from lp.codehosting.scanner.bzrsync import BzrSync
 from lp.services.config import config
 from lp.services.database.interfaces import IStore
+from lp.services.features.testing import FeatureFixture
 from lp.services.osutils import override_environ
 from lp.testing import TestCaseWithFactory
 from lp.testing.dbuser import (
@@ -743,6 +749,32 @@
         self.assertEqual(False, recipe.is_stale)
 
 
+class TestTriggerWebhooks(BzrSyncTestCase):
+    """Test triggering of webhooks."""
+
+    def test_triggers_webhooks(self):
+        # On tip change, any relevant webhooks are triggered.
+        self.useFixture(FeatureFixture({"code.bzr.webhooks.enabled": "on"}))
+        self.syncAndCount()
+        old_revid = self.db_branch.last_scanned_id
+        with dbuser(config.launchpad.dbuser):
+            hook = self.factory.makeWebhook(
+                target=self.db_branch, event_types=["bzr:push:0.1"])
+        self.commitRevision()
+        new_revid = self.bzr_branch.last_revision()
+        self.makeBzrSync(self.db_branch).syncBranchAndClose()
+        delivery = hook.deliveries.one()
+        self.assertThat(
+            delivery,
+            MatchesStructure(
+                event_type=Equals("bzr:push:0.1"),
+                payload=MatchesDict({
+                    "bzr_branch_path": Equals(self.db_branch.unique_name),
+                    "old": Equals({"revision_id": old_revid}),
+                    "new": Equals({"revision_id": new_revid}),
+                    })))
+
+
 class TestRevisionProperty(BzrSyncTestCase):
     """Tests for storting revision properties."""
 

=== 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-24 13:55:55 +0000
@@ -1,7 +1,7 @@
 # Copyright 2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-"""Communication with the Git hosting service."""
+"""Communication with webhook delivery endpoints."""
 
 __metaclass__ = type
 __all__ = [

=== 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-24 13:55:55 +0000
@@ -71,13 +71,14 @@
 
 
 WEBHOOK_EVENT_TYPES = {
+    "bzr:push:0.1": "Bazaar push",
     "git:push:0.1": "Git push",
     }
 
 
 @error_status(httplib.UNAUTHORIZED)
 class WebhookFeatureDisabled(Exception):
-    """Only certain users can create new Git repositories."""
+    """Only certain users can create new webhooks."""
 
     def __init__(self):
         Exception.__init__(

=== 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-24 13:55:55 +0000
@@ -93,6 +93,9 @@
     git_repository_id = Int(name='git_repository')
     git_repository = Reference(git_repository_id, 'GitRepository.id')
 
+    branch_id = Int(name='branch')
+    branch = Reference(branch_id, 'Branch.id')
+
     registrant_id = Int(name='registrant', allow_none=False)
     registrant = Reference(registrant_id, 'Person.id')
     date_created = DateTime(tzinfo=utc, allow_none=False)
@@ -108,6 +111,8 @@
     def target(self):
         if self.git_repository is not None:
             return self.git_repository
+        elif self.branch is not None:
+            return self.branch
         else:
             raise AssertionError("No target.")
 
@@ -159,10 +164,13 @@
 
     def new(self, target, registrant, delivery_url, event_types, active,
             secret):
+        from lp.code.interfaces.branch import IBranch
         from lp.code.interfaces.gitrepository import IGitRepository
         hook = Webhook()
         if IGitRepository.providedBy(target):
             hook.git_repository = target
+        elif IBranch.providedBy(target):
+            hook.branch = target
         else:
             raise AssertionError("Unsupported target: %r" % (target,))
         hook.registrant = registrant
@@ -184,9 +192,12 @@
         return IStore(Webhook).get(Webhook, id)
 
     def findByTarget(self, target):
+        from lp.code.interfaces.branch import IBranch
         from lp.code.interfaces.gitrepository import IGitRepository
         if IGitRepository.providedBy(target):
             target_filter = Webhook.git_repository == target
+        elif IBranch.providedBy(target):
+            target_filter = Webhook.branch == target
         else:
             raise AssertionError("Unsupported target: %r" % (target,))
         return IStore(Webhook).find(Webhook, target_filter).order_by(

=== 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-24 13:55:55 +0000
@@ -18,8 +18,8 @@
       <div>
         <div class="beta" style="display: inline">
           <img class="beta" alt="[BETA]" src="/@@/beta" /></div>
-        The only currently supported events are Git pushes. We'll be
-        rolling out webhooks for more soon.
+        The only currently supported events are Git and Bazaar pushes. We'll
+        be rolling out webhooks for more soon.
       </div>
       <ul class="horizontal">
         <li>

=== 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-24 13:55:55 +0000
@@ -27,6 +27,7 @@
 from lp.testing.matchers import HasQueryCount
 from lp.testing.views import create_view
 
+
 breadcrumbs_tag = soupmatchers.Tag(
     'breadcrumbs', 'ol', attrs={'class': 'breadcrumbs'})
 webhooks_page_crumb_tag = soupmatchers.Tag(
@@ -47,12 +48,28 @@
     'batch nav links', 'td', attrs={'class': 'batch-navigation-links'})
 
 
+class GitRepositoryTestHelpers:
+
+    event_type = "git:push:0.1"
+
+    def makeTarget(self):
+        return self.factory.makeGitRepository()
+
+
+class BranchTestHelpers:
+
+    event_type = "bzr:push:0.1"
+
+    def makeTarget(self):
+        return self.factory.makeBranch()
+
+
 class WebhookTargetViewTestHelpers:
 
     def setUp(self):
         super(WebhookTargetViewTestHelpers, self).setUp()
         self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
-        self.target = self.factory.makeGitRepository()
+        self.target = self.makeTarget()
         self.owner = self.target.owner
         login_person(self.owner)
 
@@ -65,7 +82,7 @@
         return view
 
 
-class TestWebhooksView(WebhookTargetViewTestHelpers, TestCaseWithFactory):
+class TestWebhooksViewBase(WebhookTargetViewTestHelpers):
 
     layer = DatabaseFunctionalLayer
 
@@ -125,7 +142,19 @@
         self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
 
 
-class TestWebhookAddView(WebhookTargetViewTestHelpers, TestCaseWithFactory):
+class TestWebhooksViewGitRepository(
+    TestWebhooksViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
+
+    pass
+
+
+class TestWebhooksViewBranch(
+    TestWebhooksViewBase, BranchTestHelpers, TestCaseWithFactory):
+
+    pass
+
+
+class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
 
     layer = DatabaseFunctionalLayer
 
@@ -150,7 +179,7 @@
             form={
                 "field.delivery_url": "http://example.com/test";,
                 "field.active": "on", "field.event_types-empty-marker": "1",
-                "field.event_types": "git:push:0.1",
+                "field.event_types": self.event_type,
                 "field.actions.new": "Add webhook"})
         self.assertEqual([], view.errors)
         hook = self.target.webhooks.one()
@@ -161,7 +190,7 @@
                 registrant=self.owner,
                 delivery_url="http://example.com/test";,
                 active=True,
-                event_types=["git:push:0.1"]))
+                event_types=[self.event_type]))
 
     def test_rejects_bad_scheme(self):
         transaction.commit()
@@ -176,12 +205,24 @@
         self.assertIs(None, self.target.webhooks.one())
 
 
+class TestWebhookAddViewGitRepository(
+    TestWebhookAddViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
+
+    pass
+
+
+class TestWebhookAddViewBranch(
+    TestWebhookAddViewBase, BranchTestHelpers, TestCaseWithFactory):
+
+    pass
+
+
 class WebhookViewTestHelpers:
 
     def setUp(self):
         super(WebhookViewTestHelpers, self).setUp()
         self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
-        self.target = self.factory.makeGitRepository()
+        self.target = self.makeTarget()
         self.owner = self.target.owner
         self.webhook = self.factory.makeWebhook(
             target=self.target, delivery_url=u'http://example.com/original')
@@ -196,7 +237,7 @@
         return view
 
 
-class TestWebhookView(WebhookViewTestHelpers, TestCaseWithFactory):
+class TestWebhookViewBase(WebhookViewTestHelpers):
 
     layer = DatabaseFunctionalLayer
 
@@ -249,7 +290,19 @@
                 event_types=[]))
 
 
-class TestWebhookDeleteView(WebhookViewTestHelpers, TestCaseWithFactory):
+class TestWebhookViewGitRepository(
+    TestWebhookViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
+
+    pass
+
+
+class TestWebhookViewBranch(
+    TestWebhookViewBase, BranchTestHelpers, TestCaseWithFactory):
+
+    pass
+
+
+class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
 
     layer = DatabaseFunctionalLayer
 
@@ -281,3 +334,15 @@
             form={"field.actions.delete": "Delete webhook"})
         self.assertEqual([], view.errors)
         self.assertIs(None, self.target.webhooks.one())
+
+
+class TestWebhookDeleteViewGitRepository(
+    TestWebhookDeleteViewBase, GitRepositoryTestHelpers, TestCaseWithFactory):
+
+    pass
+
+
+class TestWebhookDeleteViewBranch(
+    TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory):
+
+    pass

=== 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-24 13:55:55 +0000
@@ -115,17 +115,17 @@
             expected_set_permissions, checker.set_permissions, 'set')
 
 
-class TestWebhookSet(TestCaseWithFactory):
+class TestWebhookSetBase:
 
     layer = DatabaseFunctionalLayer
 
     def test_new(self):
-        target = self.factory.makeGitRepository()
+        target = self.makeTarget()
         login_person(target.owner)
         person = self.factory.makePerson()
         hook = getUtility(IWebhookSet).new(
-            target, person, u'http://path/to/something', ['git:push'], True,
-            u'sekrit')
+            target, person, u'http://path/to/something', [self.event_type],
+            True, u'sekrit')
         Store.of(hook).flush()
         self.assertEqual(target, hook.target)
         self.assertEqual(person, hook.registrant)
@@ -134,7 +134,7 @@
         self.assertEqual(u'http://path/to/something', hook.delivery_url)
         self.assertEqual(True, hook.active)
         self.assertEqual(u'sekrit', hook.secret)
-        self.assertEqual(['git:push'], hook.event_types)
+        self.assertEqual([self.event_type], hook.event_types)
 
     def test_getByID(self):
         hook1 = self.factory.makeWebhook()
@@ -148,8 +148,8 @@
                 None, getUtility(IWebhookSet).getByID(1234))
 
     def test_findByTarget(self):
-        target1 = self.factory.makeGitRepository()
-        target2 = self.factory.makeGitRepository()
+        target1 = self.makeTarget()
+        target2 = self.makeTarget()
         for target, name in ((target1, 'one'), (target2, 'two')):
             for i in range(3):
                 self.factory.makeWebhook(
@@ -168,7 +168,7 @@
                 getUtility(IWebhookSet).findByTarget(target2)])
 
     def test_delete(self):
-        target = self.factory.makeGitRepository()
+        target = self.makeTarget()
         login_person(target.owner)
         hooks = []
         for i in range(3):
@@ -195,21 +195,21 @@
 
     def test_trigger(self):
         owner = self.factory.makePerson()
-        target1 = self.factory.makeGitRepository(owner=owner)
-        target2 = self.factory.makeGitRepository(owner=owner)
+        target1 = self.makeTarget(owner=owner)
+        target2 = self.makeTarget(owner=owner)
         hook1a = self.factory.makeWebhook(
             target=target1, event_types=[])
         hook1b = self.factory.makeWebhook(
-            target=target1, event_types=['git:push:0.1'])
+            target=target1, event_types=[self.event_type])
         hook2a = self.factory.makeWebhook(
-            target=target2, event_types=['git:push:0.1'])
+            target=target2, event_types=[self.event_type])
         hook2b = self.factory.makeWebhook(
-            target=target2, event_types=['git:push:0.1'], active=False)
+            target=target2, event_types=[self.event_type], active=False)
 
         # Only webhooks subscribed to the relevant target and event type
         # are triggered.
         getUtility(IWebhookSet).trigger(
-            target1, 'git:push:0.1', {'some': 'payload'})
+            target1, self.event_type, {'some': 'payload'})
         with admin_logged_in():
             self.assertThat(list(hook1a.deliveries), HasLength(0))
             self.assertThat(list(hook1b.deliveries), HasLength(1))
@@ -220,7 +220,7 @@
 
         # Disabled webhooks aren't triggered.
         getUtility(IWebhookSet).trigger(
-            target2, 'git:push:0.1', {'other': 'payload'})
+            target2, self.event_type, {'other': 'payload'})
         with admin_logged_in():
             self.assertThat(list(hook1a.deliveries), HasLength(0))
             self.assertThat(list(hook1b.deliveries), HasLength(1))
@@ -228,3 +228,19 @@
             self.assertThat(list(hook2b.deliveries), HasLength(0))
             delivery = hook2a.deliveries.one()
             self.assertEqual(delivery.payload, {'other': 'payload'})
+
+
+class TestWebhookSetGitRepository(TestWebhookSetBase, TestCaseWithFactory):
+
+    event_type = 'git:push:0.1'
+
+    def makeTarget(self, owner=None):
+        return self.factory.makeGitRepository(owner=owner)
+
+
+class TestWebhookSetBranch(TestWebhookSetBase, TestCaseWithFactory):
+
+    event_type = 'bzr:push:0.1'
+
+    def makeTarget(self, owner=None):
+        return self.factory.makeBranch(owner=owner)

=== 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-24 13:55:55 +0000
@@ -260,12 +260,12 @@
         self.assertIs(None, representation['date_first_sent'])
 
 
-class TestWebhookTarget(TestCaseWithFactory):
+class TestWebhookTargetBase:
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        super(TestWebhookTarget, self).setUp()
-        self.target = self.factory.makeGitRepository()
+        super(TestWebhookTargetBase, self).setUp()
+        self.target = self.makeTarget()
         self.owner = self.target.owner
         self.target_url = api_url(self.target)
         self.webservice = webservice_for_person(
@@ -309,13 +309,13 @@
         response = self.webservice.named_post(
             self.target_url, 'newWebhook',
             delivery_url='http://example.com/ep',
-            event_types=['git:push:0.1'], api_version='devel')
+            event_types=[self.event_type], api_version='devel')
         self.assertEqual(201, response.status)
 
         representation = self.webservice.get(
             self.target_url + '/webhooks', api_version='devel').jsonBody()
         self.assertContentEqual(
-            [('http://example.com/ep', ['git:push:0.1'], True)],
+            [('http://example.com/ep', [self.event_type], True)],
             [(entry['delivery_url'], entry['event_types'], entry['active'])
              for entry in representation['entries']])
 
@@ -323,8 +323,9 @@
         self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true'}))
         response = self.webservice.named_post(
             self.target_url, 'newWebhook',
-            delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
-            secret='sekrit', api_version='devel')
+            delivery_url='http://example.com/ep',
+            event_types=[self.event_type], secret='sekrit',
+            api_version='devel')
         self.assertEqual(201, response.status)
 
         # The secret is set, but cannot be read back through the API.
@@ -339,16 +340,33 @@
         webservice = LaunchpadWebServiceCaller()
         response = webservice.named_post(
             self.target_url, 'newWebhook',
-            delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
-            api_version='devel')
+            delivery_url='http://example.com/ep',
+            event_types=[self.event_type], api_version='devel')
         self.assertEqual(401, response.status)
         self.assertIn('launchpad.Edit', response.body)
 
     def test_newWebhook_feature_flag_guard(self):
         response = self.webservice.named_post(
             self.target_url, 'newWebhook',
-            delivery_url='http://example.com/ep', event_types=['git:push:0.1'],
-            api_version='devel')
+            delivery_url='http://example.com/ep',
+            event_types=[self.event_type], api_version='devel')
         self.assertEqual(401, response.status)
         self.assertEqual(
             'This webhook feature is not available yet.', response.body)
+
+
+class TestWebhookTargetGitRepository(
+    TestWebhookTargetBase, TestCaseWithFactory):
+
+    event_type = 'git:push:0.1'
+
+    def makeTarget(self):
+        return self.factory.makeGitRepository()
+
+
+class TestWebhookTargetBranch(TestWebhookTargetBase, TestCaseWithFactory):
+
+    event_type = 'bzr:push:0.1'
+
+    def makeTarget(self):
+        return self.factory.makeBranch()


Follow ups