launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27252
[Merge] ~cjwatson/launchpad:oci-recipe-build-export-cancel-rescore into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:oci-recipe-build-export-cancel-rescore into launchpad:master with ~cjwatson/launchpad:snap-oci-build-privacy-banner as a prerequisite.
Commit message:
Export OCIRecipeBuild.cancel and OCIRecipeBuild.rescore
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/405340
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:oci-recipe-build-export-cancel-rescore into launchpad:master.
diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
index ad659ea..2065558 100644
--- a/lib/lp/oci/interfaces/ocirecipebuild.py
+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
@@ -26,6 +26,7 @@ from lazr.restful.declarations import (
exported,
exported_as_webservice_entry,
operation_for_version,
+ operation_parameters,
)
from lazr.restful.fields import (
CollectionField,
@@ -296,6 +297,8 @@ class IOCIRecipeBuildEdit(Interface):
non-scored BuildQueue entry is created for it.
"""
+ @export_write_operation()
+ @operation_for_version("devel")
def cancel():
"""Cancel the build if it is either pending or in progress.
@@ -314,6 +317,9 @@ class IOCIRecipeBuildEdit(Interface):
class IOCIRecipeBuildAdmin(Interface):
"""`IOCIRecipeBuild` attributes that require launchpad.Admin permission."""
+ @operation_parameters(score=Int(title=_("Score"), required=True))
+ @export_write_operation()
+ @operation_for_version("devel")
def rescore(score):
"""Change the build's score."""
diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py
index 05f9e12..46aa803 100644
--- a/lib/lp/oci/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/tests/test_ocirecipebuild.py
@@ -14,6 +14,7 @@ from fixtures import FakeLogger
from pymacaroons import Macaroon
import pytz
import six
+from six.moves.urllib.request import urlopen
from testtools.matchers import (
ContainsDict,
Equals,
@@ -28,7 +29,10 @@ from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
from lp.app.errors import NotFoundError
-from lp.app.interfaces.launchpad import IPrivacy
+from lp.app.interfaces.launchpad import (
+ ILaunchpadCelebrities,
+ IPrivacy,
+ )
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.buildqueue import IBuildQueue
from lp.buildmaster.interfaces.packagebuild import IPackageBuild
@@ -62,10 +66,15 @@ from lp.services.macaroons.interfaces import (
)
from lp.services.macaroons.testing import MacaroonTestMixin
from lp.services.propertycache import clear_property_cache
+from lp.services.webapp.interfaces import OAuthPermission
from lp.services.webapp.publisher import canonical_url
from lp.services.webhooks.testing import LogsScheduledWebhooks
from lp.testing import (
admin_logged_in,
+ ANONYMOUS,
+ api_url,
+ login,
+ logout,
person_logged_in,
StormStatementRecorder,
TestCaseWithFactory,
@@ -73,9 +82,11 @@ from lp.testing import (
from lp.testing.dbuser import dbuser
from lp.testing.layers import (
DatabaseFunctionalLayer,
+ LaunchpadFunctionalLayer,
LaunchpadZopelessLayer,
)
from lp.testing.matchers import HasQueryCount
+from lp.testing.pages import webservice_for_person
from lp.xmlrpc.interfaces import IPrivateApplication
@@ -266,7 +277,7 @@ class TestOCIRecipeBuild(OCIConfigHelperMixin, TestCaseWithFactory):
self.assertEqual(bq, self.build.buildqueue_record)
def test_is_private(self):
- # An OCIRecipeBuild is private if its owner is.
+ # An OCIRecipeBuild is private if its recipe or owner is.
self.assertFalse(self.build.is_private)
self.assertFalse(self.build.private)
private_team = self.factory.makeTeam(
@@ -693,6 +704,152 @@ class TestOCIRecipeBuildSet(TestCaseWithFactory):
self.assertFalse(target.virtualized)
+class TestOCIRecipeBuildWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(TestOCIRecipeBuildWebservice, self).setUp()
+ self.setConfig()
+ self.person = self.factory.makePerson()
+ self.webservice = webservice_for_person(
+ self.person, permission=OAuthPermission.WRITE_PRIVATE)
+ self.webservice.default_api_version = "devel"
+ login(ANONYMOUS)
+
+ def getURL(self, obj):
+ return self.webservice.getAbsoluteUrl(api_url(obj))
+
+ def test_properties(self):
+ # The basic properties of an OCIRecipeBuild are sensible.
+ db_build = self.factory.makeOCIRecipeBuild(requester=self.person)
+ build_url = api_url(db_build)
+ logout()
+ build = self.webservice.get(build_url).jsonBody()
+ with person_logged_in(self.person):
+ self.assertThat(build, ContainsDict({
+ "requester_link": Equals(self.getURL(self.person)),
+ "recipe_link": Equals(self.getURL(db_build.recipe)),
+ "distro_arch_series_link": Equals(
+ self.getURL(db_build.distro_arch_series)),
+ "score": Is(None),
+ "can_be_rescored": Is(False),
+ "can_be_cancelled": Is(False),
+ }))
+
+ def test_public(self):
+ # An OCIRecipeBuild with a public recipe and repository is itself
+ # public.
+ db_build = self.factory.makeOCIRecipeBuild()
+ build_url = api_url(db_build)
+ unpriv_webservice = webservice_for_person(
+ self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
+ unpriv_webservice.default_api_version = "devel"
+ logout()
+ self.assertEqual(200, self.webservice.get(build_url).status)
+ self.assertEqual(200, unpriv_webservice.get(build_url).status)
+
+ def test_private_recipe(self):
+ # An OCIRecipeBuild with a private recipe is private.
+ db_team = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.MODERATED,
+ owner=self.person)
+ with person_logged_in(self.person):
+ db_build = self.factory.makeOCIRecipeBuild(
+ requester=self.person, owner=db_team,
+ information_type=InformationType.USERDATA)
+ build_url = api_url(db_build)
+ unpriv_webservice = webservice_for_person(
+ self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
+ unpriv_webservice.default_api_version = "devel"
+ logout()
+ response = self.webservice.get(build_url)
+ self.assertEqual(200, response.status)
+ self.assertEqual(401, unpriv_webservice.get(build_url).status)
+
+ def test_private_recipe_owner(self):
+ # An OCIRecipeBuild with a private recipe owner is private.
+ db_team = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.MODERATED,
+ owner=self.person, visibility=PersonVisibility.PRIVATE)
+ with person_logged_in(self.person):
+ db_build = self.factory.makeOCIRecipeBuild(
+ requester=self.person, owner=db_team,
+ information_type=InformationType.USERDATA)
+ build_url = api_url(db_build)
+ unpriv_webservice = webservice_for_person(
+ self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
+ unpriv_webservice.default_api_version = "devel"
+ logout()
+ response = self.webservice.get(build_url)
+ self.assertEqual(200, response.status)
+ # 404 since we aren't allowed to know that the private team exists.
+ self.assertEqual(404, unpriv_webservice.get(build_url).status)
+
+ def test_cancel(self):
+ # The owner of a build can cancel it.
+ db_build = self.factory.makeOCIRecipeBuild(requester=self.person)
+ db_build.queueBuild()
+ build_url = api_url(db_build)
+ unpriv_webservice = webservice_for_person(
+ self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
+ unpriv_webservice.default_api_version = "devel"
+ logout()
+ build = self.webservice.get(build_url).jsonBody()
+ self.assertTrue(build["can_be_cancelled"])
+ response = unpriv_webservice.named_post(build["self_link"], "cancel")
+ self.assertEqual(401, response.status)
+ response = self.webservice.named_post(build["self_link"], "cancel")
+ self.assertEqual(200, response.status)
+ build = self.webservice.get(build_url).jsonBody()
+ self.assertFalse(build["can_be_cancelled"])
+ with person_logged_in(self.person):
+ self.assertEqual(BuildStatus.CANCELLED, db_build.status)
+
+ def test_rescore(self):
+ # Buildd administrators can rescore builds.
+ db_build = self.factory.makeOCIRecipeBuild(requester=self.person)
+ db_build.queueBuild()
+ build_url = api_url(db_build)
+ buildd_admin = self.factory.makePerson(
+ member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
+ buildd_admin_webservice = webservice_for_person(
+ buildd_admin, permission=OAuthPermission.WRITE_PUBLIC)
+ buildd_admin_webservice.default_api_version = "devel"
+ logout()
+ build = self.webservice.get(build_url).jsonBody()
+ self.assertEqual(2510, build["score"])
+ self.assertTrue(build["can_be_rescored"])
+ response = self.webservice.named_post(
+ build["self_link"], "rescore", score=5000)
+ self.assertEqual(401, response.status)
+ response = buildd_admin_webservice.named_post(
+ build["self_link"], "rescore", score=5000)
+ self.assertEqual(200, response.status)
+ build = self.webservice.get(build_url).jsonBody()
+ self.assertEqual(5000, build["score"])
+
+ def assertCanOpenRedirectedUrl(self, browser, url):
+ browser.open(url)
+ self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
+ urlopen(browser.headers["Location"]).close()
+
+ def test_logs(self):
+ # API clients can fetch the build and upload logs.
+ db_build = self.factory.makeOCIRecipeBuild(requester=self.person)
+ db_build.setLog(self.factory.makeLibraryFileAlias("buildlog.txt.gz"))
+ db_build.storeUploadLog("uploaded")
+ build_url = api_url(db_build)
+ logout()
+ build = self.webservice.get(build_url).jsonBody()
+ browser = self.getNonRedirectingBrowser(user=self.person)
+ browser.raiseHttpErrors = False
+ self.assertIsNotNone(build["build_log_url"])
+ self.assertCanOpenRedirectedUrl(browser, build["build_log_url"])
+ self.assertIsNotNone(build["upload_log_url"])
+ self.assertCanOpenRedirectedUrl(browser, build["upload_log_url"])
+
+
class TestOCIRecipeBuildMacaroonIssuer(
MacaroonTestMixin, OCIConfigHelperMixin, TestCaseWithFactory):
"""Test OCIRecipeBuild macaroon issuing and verification."""