launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32461
[Merge] ~alvarocs/launchpad:validate-mp-subscopes into launchpad:master
Alvaro Crespo Serrano has proposed merging ~alvarocs/launchpad:validate-mp-subscopes into launchpad:master.
Commit message:
Enable merge proposal subscopes through Launchpad API
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~alvarocs/launchpad/+git/launchpad/+merge/485489
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~alvarocs/launchpad:validate-mp-subscopes into launchpad:master.
diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
index 20fdf26..ab07555 100644
--- a/lib/lp/code/model/branch.py
+++ b/lib/lp/code/model/branch.py
@@ -220,7 +220,16 @@ class Branch(StormBase, WebhookTargetMixin, BzrIdentityMixin):
@property
def valid_webhook_event_types(self):
- return ["bzr:push:0.1", "merge-proposal:0.1"]
+ return [
+ "bzr:push:0.1",
+ "merge-proposal:0.1",
+ "merge-proposal:0.1::create",
+ "merge-proposal:0.1::push",
+ "merge-proposal:0.1::review",
+ "merge-proposal:0.1::edit",
+ "merge-proposal:0.1::status-change",
+ "merge-proposal:0.1::delete",
+ ]
@property
def default_webhook_event_types(self):
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 440b270..483f986 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -417,7 +417,17 @@ class GitRepository(
@property
def valid_webhook_event_types(self):
- return ["ci:build:0.1", "git:push:0.1", "merge-proposal:0.1"]
+ return [
+ "ci:build:0.1",
+ "git:push:0.1",
+ "merge-proposal:0.1",
+ "merge-proposal:0.1::create",
+ "merge-proposal:0.1::push",
+ "merge-proposal:0.1::review",
+ "merge-proposal:0.1::edit",
+ "merge-proposal:0.1::status-change",
+ "merge-proposal:0.1::delete",
+ ]
@property
def default_webhook_event_types(self):
diff --git a/lib/lp/services/webhooks/browser.py b/lib/lp/services/webhooks/browser.py
index 00be986..2763f2c 100644
--- a/lib/lp/services/webhooks/browser.py
+++ b/lib/lp/services/webhooks/browser.py
@@ -12,6 +12,8 @@ from lazr.restful.interface import use_template
from lazr.restful.interfaces import IJSONRequestCache
from zope.component import getUtility
from zope.interface import Interface
+from zope.schema import Choice, List
+from zope.schema.vocabulary import SimpleVocabulary
from lp.app.browser.launchpadform import (
LaunchpadEditFormView,
@@ -33,7 +35,29 @@ from lp.services.webapp.batching import (
get_batch_properties_for_json_cache,
)
from lp.services.webapp.breadcrumb import Breadcrumb
-from lp.services.webhooks.interfaces import IWebhook, IWebhookSet
+from lp.services.webhooks.interfaces import (
+ WEBHOOK_EVENT_TYPES,
+ IWebhook,
+ IWebhookSet,
+)
+
+
+class UIValidWebhookEventTypeVocabulary(SimpleVocabulary):
+ """A UI-specific vocabulary that excludes subscopes for form views."""
+
+ def __init__(self, context):
+ if IWebhook.providedBy(context):
+ target = context.target
+ else:
+ target = context
+
+ terms = []
+ for key in target.valid_webhook_event_types:
+ if "::" not in key:
+ terms.append(
+ self.createTerm(key, key, WEBHOOK_EVENT_TYPES[key])
+ )
+ super().__init__(terms)
class WebhookNavigation(Navigation):
@@ -104,13 +128,18 @@ class WebhookEditSchema(Interface):
IWebhook,
include=[
"delivery_url",
- "event_types",
"active",
"secret",
"git_ref_pattern",
],
)
+ event_types = List(
+ title="Event types",
+ required=True,
+ value_type=Choice(vocabulary="UIValidWebhookEventType"),
+ )
+
class WebhookAddView(LaunchpadFormView):
page_title = label = "Add webhook"
diff --git a/lib/lp/services/webhooks/configure.zcml b/lib/lp/services/webhooks/configure.zcml
index 6dbf8d6..de2dda9 100644
--- a/lib/lp/services/webhooks/configure.zcml
+++ b/lib/lp/services/webhooks/configure.zcml
@@ -41,6 +41,17 @@
<class class="lp.services.webhooks.interfaces.ValidWebhookEventTypeVocabulary">
<allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
</class>
+
+ <lp:securedutility
+ name="UIValidWebhookEventType"
+ component="lp.services.webhooks.browser.UIValidWebhookEventTypeVocabulary"
+ provides="zope.schema.interfaces.IVocabularyFactory">
+ <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
+ </lp:securedutility>
+
+ <class class="lp.services.webhooks.browser.UIValidWebhookEventTypeVocabulary">
+ <allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
+ </class>
<lp:securedutility
component="lp.services.webhooks.model.WebhookJob"
diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py
index 69b5978..789f7f0 100644
--- a/lib/lp/services/webhooks/interfaces.py
+++ b/lib/lp/services/webhooks/interfaces.py
@@ -38,6 +38,7 @@ from zope.schema import Bool, Choice, Datetime, Dict, Int, List, TextLine
from zope.schema.vocabulary import SimpleVocabulary
from lp import _
+from lp.app.validators import LaunchpadValidationError
from lp.registry.interfaces.person import IPerson
from lp.services.fields import URIField
from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
@@ -57,12 +58,12 @@ WEBHOOK_EVENT_TYPES = {
"livefs:build:0.1": "Live filesystem build",
"merge-proposal:0.1": "Merge proposal",
# Merge proposal subscopes
- "merge-proposal:0.1::create": "Create",
- "merge-proposal:0.1::push": "Push",
- "merge-proposal:0.1::review": "Review",
- "merge-proposal:0.1::edit": "Edit",
- "merge-proposal:0.1::status-change": "Status Change",
- "merge-proposal:0.1::delete": "Delete",
+ "merge-proposal:0.1::create": "Merge proposal Created",
+ "merge-proposal:0.1::push": "Merge proposal Pushed",
+ "merge-proposal:0.1::review": "Merge proposal Reviewed",
+ "merge-proposal:0.1::edit": "Merge proposal Edited",
+ "merge-proposal:0.1::status-change": "Merge proposal Status Changed",
+ "merge-proposal:0.1::delete": "Merge proposal Deleted",
"oci-recipe:build:0.1": "OCI recipe build",
"snap:build:0.1": "Snap build",
"craft-recipe:build:0.1": "Craft recipe build",
@@ -105,6 +106,19 @@ class ValidWebhookEventTypeVocabulary(SimpleVocabulary):
super().__init__(terms)
+def validate_event_type_parent_subscope(event_types):
+ """Validator to handle parent and its subscopes selection."""
+ parents = {et.split("::")[0] for et in event_types if "::" in et}
+ for parent in parents:
+ if parent in event_types:
+ raise LaunchpadValidationError(
+ f"Both the parent {parent} and any of its subscopes cannot "
+ "be selected. Please, select either the parent or one or "
+ "more subscopes."
+ )
+ return True
+
+
@exported_as_webservice_entry(as_of="beta")
class IWebhook(Interface):
id = Int(title=_("ID"), readonly=True, required=True)
@@ -126,6 +140,7 @@ class IWebhook(Interface):
title=_("Event types"),
required=True,
readonly=False,
+ constraint=validate_event_type_parent_subscope,
)
)
registrant = exported(
diff --git a/lib/lp/services/webhooks/tests/test_webservice.py b/lib/lp/services/webhooks/tests/test_webservice.py
index 53f7e93..033f815 100644
--- a/lib/lp/services/webhooks/tests/test_webservice.py
+++ b/lib/lp/services/webhooks/tests/test_webservice.py
@@ -260,6 +260,62 @@ class TestWebhook(TestCaseWithFactory):
with person_logged_in(self.owner):
self.assertIs(None, self.webhook.secret)
+ def test_patch_event_types_allows_only_parent_scope(self):
+ # Parent scope alone should be accepted
+ patch = json.dumps({"event_types": ["merge-proposal:0.1"]})
+ response = self.webservice.patch(
+ self.webhook_url, "application/json", patch, api_version="devel"
+ )
+ self.assertEqual(209, response.status)
+ representation = self.webservice.get(
+ self.webhook_url, api_version="devel"
+ ).jsonBody()
+ self.assertEqual(["merge-proposal:0.1"], representation["event_types"])
+
+ def test_patch_event_types_allows_only_subscopes(self):
+ # Subscopes alone should be accepted
+ patch = json.dumps(
+ {
+ "event_types": [
+ "" "merge-proposal:0.1::create",
+ "merge-proposal:0.1::edit",
+ ]
+ }
+ )
+ response = self.webservice.patch(
+ self.webhook_url, "application/json", patch, api_version="devel"
+ )
+ self.assertEqual(209, response.status)
+ representation = self.webservice.get(
+ self.webhook_url, api_version="devel"
+ ).jsonBody()
+ self.assertEqual(
+ ["merge-proposal:0.1::create", "merge-proposal:0.1::edit"],
+ representation["event_types"],
+ )
+
+ def test_patch_event_types_rejects_parent_and_subscopes(self):
+ # A parent and one of its subscopes cannot be selected together
+ patch = json.dumps(
+ {
+ "event_types": [
+ "merge-proposal:0.1",
+ "merge-proposal:0.1::create",
+ ]
+ }
+ )
+ response = self.webservice.patch(
+ self.webhook_url,
+ "application/json",
+ patch,
+ api_version="devel",
+ )
+ self.assertEqual(400, response.status)
+ self.assertIn(
+ b"Both the parent merge-proposal:0.1 and any of its subscopes",
+ response.body,
+ )
+
class TestWebhookDelivery(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
Follow ups