launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #23278
[Merge] lp:~cjwatson/launchpad/base-snap into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/base-snap into lp:launchpad with lp:~cjwatson/launchpad/snap-only-available-processors as a prerequisite.
Commit message:
Add BaseSnap model.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1812985 in Launchpad itself: "Support snapcraft base snaps"
https://bugs.launchpad.net/launchpad/+bug/1812985
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/base-snap/+merge/362729
The corresponding DB patch is in https://code.launchpad.net/~cjwatson/launchpad/db-base-snap/+merge/362727.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/base-snap into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2018-12-03 13:42:36 +0000
+++ database/schema/security.cfg 2019-02-05 12:19:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
#
# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
@@ -128,6 +128,7 @@
public.archivejob = SELECT, INSERT, UPDATE, DELETE
public.archivepermission = SELECT, INSERT, UPDATE, DELETE
public.archivesubscriber = SELECT, INSERT, UPDATE
+public.basesnap = SELECT, INSERT, UPDATE, DELETE
public.binaryandsourcepackagenameview = SELECT
public.binarypackagepublishinghistory = SELECT
public.binarypackagereleasedownloadcount= SELECT, INSERT, UPDATE
@@ -949,6 +950,7 @@
public.archive = SELECT, UPDATE
public.archivearch = SELECT, UPDATE
public.archivedependency = SELECT
+public.basesnap = SELECT
public.binarypackagebuild = SELECT, INSERT, UPDATE
public.binarypackagefile = SELECT
public.binarypackagename = SELECT
@@ -1343,6 +1345,7 @@
public.archivefile = SELECT
public.archivejob = SELECT, INSERT
public.archivepermission = SELECT
+public.basesnap = SELECT
public.binarypackagebuild = SELECT, INSERT, UPDATE
public.binarypackagefile = SELECT, INSERT
public.binarypackagename = SELECT, INSERT
@@ -2179,6 +2182,7 @@
public.archiveauthtoken = SELECT, UPDATE
public.archivepermission = SELECT, UPDATE
public.archivesubscriber = SELECT, UPDATE
+public.basesnap = SELECT, UPDATE
public.binarypackagepublishinghistory = SELECT, UPDATE
public.branch = SELECT, UPDATE
public.branchmergeproposal = SELECT, UPDATE
@@ -2316,6 +2320,7 @@
public.accesspolicygrant = SELECT, DELETE
public.account = SELECT, DELETE
public.answercontact = SELECT, DELETE
+public.basesnap = SELECT
public.branch = SELECT, UPDATE
public.branchjob = SELECT, DELETE
public.binarypackagename = SELECT
@@ -2553,6 +2558,7 @@
groups=script
public.account = SELECT
public.archive = SELECT
+public.basesnap = SELECT
public.branch = SELECT
public.builder = SELECT
public.buildfarmjob = SELECT, INSERT
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2018-07-15 16:23:15 +0000
+++ lib/lp/app/browser/launchpad.py 2019-02-05 12:19:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Browser code for the launchpad application."""
@@ -157,6 +157,7 @@
from lp.services.webapp.url import urlappend
from lp.services.worlddata.interfaces.country import ICountrySet
from lp.services.worlddata.interfaces.language import ILanguageSet
+from lp.snappy.interfaces.basesnap import IBaseSnapSet
from lp.snappy.interfaces.snap import ISnapSet
from lp.snappy.interfaces.snappyseries import ISnappySeriesSet
from lp.soyuz.interfaces.archive import IArchiveSet
@@ -843,6 +844,7 @@
'+announcements': IAnnouncementSet,
'archives': IArchiveSet,
'+services': IServiceFactory,
+ '+base-snaps': IBaseSnapSet,
'binarypackagenames': IBinaryPackageNameSet,
'branches': IBranchSet,
'bugs': IMaloneApplication,
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2018-11-18 18:30:00 +0000
+++ lib/lp/security.py 2019-02-05 12:19:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Security policies for using content objects."""
@@ -197,6 +197,10 @@
ILanguage,
ILanguageSet,
)
+from lp.snappy.interfaces.basesnap import (
+ IBaseSnap,
+ IBaseSnapSet,
+ )
from lp.snappy.interfaces.snap import (
ISnap,
ISnapBuildRequest,
@@ -3362,3 +3366,16 @@
class EditSnappySeriesSet(EditByRegistryExpertsOrAdmins):
usedfor = ISnappySeriesSet
+
+
+class ViewBaseSnap(AnonymousAuthorization):
+ """Anyone can view an `IBaseSnap`."""
+ usedfor = IBaseSnap
+
+
+class EditBaseSnap(EditByRegistryExpertsOrAdmins):
+ usedfor = IBaseSnap
+
+
+class EditBaseSnapSet(EditByRegistryExpertsOrAdmins):
+ usedfor = IBaseSnapSet
=== modified file 'lib/lp/services/webservice/wadl-to-refhtml.xsl'
--- lib/lp/services/webservice/wadl-to-refhtml.xsl 2018-08-06 15:41:10 +0000
+++ lib/lp/services/webservice/wadl-to-refhtml.xsl 2019-02-05 12:19:08 +0000
@@ -208,6 +208,10 @@
<xsl:text>.</xsl:text>
<xsl:text>[component or source package].name</xsl:text>
</xsl:when>
+ <xsl:when test="@id = 'base_snap'">
+ <xsl:text>/+base-snaps/</xsl:text>
+ <var><name></var>
+ </xsl:when>
<xsl:when test="@id = 'binary_package_publishing_history'">
<xsl:text>/</xsl:text>
<var><distribution.name></var>
@@ -531,6 +535,10 @@
<xsl:text>/+build-request/</xsl:text>
<var><id></var>
</xsl:when>
+ <xsl:when test="@id = 'snappy_series'">
+ <xsl:text>/+snappy-series/</xsl:text>
+ <var><name></var>
+ </xsl:when>
<xsl:when test="@id = 'source_package'">
<xsl:text>/</xsl:text>
<var><distribution.name></var>
=== added file 'lib/lp/snappy/browser/basesnap.py'
--- lib/lp/snappy/browser/basesnap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/basesnap.py 2019-02-05 12:19:08 +0000
@@ -0,0 +1,19 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Base snap views."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ "BaseSnapSetNavigation",
+ ]
+
+from lp.services.webapp import GetitemNavigation
+from lp.snappy.interfaces.basesnap import IBaseSnapSet
+
+
+class BaseSnapSetNavigation(GetitemNavigation):
+ """Navigation methods for `IBaseSnapSet`."""
+ usedfor = IBaseSnapSet
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 2018-06-15 13:21:14 +0000
+++ lib/lp/snappy/browser/configure.zcml 2019-02-05 12:19:08 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2018 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2019 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -138,6 +138,17 @@
<browser:navigation
module="lp.snappy.browser.snappyseries"
classes="SnappySeriesSetNavigation" />
+ <browser:url
+ for="lp.snappy.interfaces.basesnap.IBaseSnap"
+ path_expression="name"
+ parent_utility="lp.snappy.interfaces.basesnap.IBaseSnapSet" />
+ <browser:url
+ for="lp.snappy.interfaces.basesnap.IBaseSnapSet"
+ path_expression="string:+base-snaps"
+ parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" />
+ <browser:navigation
+ module="lp.snappy.browser.basesnap"
+ classes="BaseSnapSetNavigation" />
<browser:page
for="*"
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2018-08-03 13:53:20 +0000
+++ lib/lp/snappy/configure.zcml 2019-02-05 12:19:08 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2018 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2019 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -130,6 +130,26 @@
interface="lp.snappy.interfaces.snappyseries.ISnappyDistroSeriesSet" />
</securedutility>
+ <!-- Base snaps -->
+ <class class="lp.snappy.model.basesnap.BaseSnap">
+ <allow
+ interface="lp.snappy.interfaces.basesnap.IBaseSnapView
+ lp.snappy.interfaces.basesnap.IBaseSnapEditableAttributes" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.snappy.interfaces.basesnap.IBaseSnapEdit"
+ set_schema="lp.snappy.interfaces.basesnap.IBaseSnapEditableAttributes" />
+ </class>
+ <securedutility
+ class="lp.snappy.model.basesnap.BaseSnapSet"
+ provides="lp.snappy.interfaces.basesnap.IBaseSnapSet">
+ <allow
+ interface="lp.snappy.interfaces.basesnap.IBaseSnapSet" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.snappy.interfaces.basesnap.IBaseSnapSetEdit" />
+ </securedutility>
+
<!-- Store interaction -->
<securedutility
class="lp.snappy.model.snapstoreclient.SnapStoreClient"
=== added file 'lib/lp/snappy/interfaces/basesnap.py'
--- lib/lp/snappy/interfaces/basesnap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/basesnap.py 2019-02-05 12:19:08 +0000
@@ -0,0 +1,231 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Base snap interfaces."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ "BaseSnapDefaultConflict",
+ "CannotDeleteBaseSnap",
+ "IBaseSnap",
+ "IBaseSnapSet",
+ "NoSuchBaseSnap",
+ ]
+
+import httplib
+
+from lazr.restful.declarations import (
+ call_with,
+ collection_default_content,
+ error_status,
+ export_as_webservice_collection,
+ export_as_webservice_entry,
+ export_destructor_operation,
+ export_factory_operation,
+ export_read_operation,
+ export_write_operation,
+ exported,
+ operation_for_version,
+ operation_parameters,
+ operation_returns_entry,
+ REQUEST_USER,
+ )
+from lazr.restful.fields import Reference
+from zope.component import getUtility
+from zope.interface import Interface
+from zope.schema import (
+ Bool,
+ Datetime,
+ Dict,
+ Int,
+ TextLine,
+ )
+
+from lp import _
+from lp.app.errors import NameLookupFailed
+from lp.app.validators.name import name_validator
+from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.services.fields import (
+ ContentNameField,
+ PublicPersonChoice,
+ Title,
+ )
+
+
+@error_status(httplib.CONFLICT)
+class BaseSnapDefaultConflict(Exception):
+ """A default base snap already exists."""
+
+
+class NoSuchBaseSnap(NameLookupFailed):
+ """The requested `BaseSnap` does not exist."""
+
+ _message_prefix = "No such base snap"
+
+
+@error_status(httplib.BAD_REQUEST)
+class CannotDeleteBaseSnap(Exception):
+ """The base snap cannot be deleted at this time."""
+
+
+class BaseSnapNameField(ContentNameField):
+ """Ensure that `IBaseSnap` has unique names."""
+
+ errormessage = _("%s is already in use by another base snap.")
+
+ @property
+ def _content_iface(self):
+ """See `UniqueField`."""
+ return IBaseSnap
+
+ def _getByName(self, name):
+ """See `ContentNameField`."""
+ try:
+ return getUtility(IBaseSnapSet).getByName(name)
+ except NoSuchBaseSnap:
+ return None
+
+
+class IBaseSnapView(Interface):
+ """`IBaseSnap` attributes that anyone can view."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+
+ date_created = exported(Datetime(
+ title=_("Date created"), required=True, readonly=True))
+
+ registrant = exported(PublicPersonChoice(
+ title=_("Registrant"), required=True, readonly=True,
+ vocabulary="ValidPersonOrTeam",
+ description=_("The person who registered this base snap.")))
+
+ is_default = exported(Bool(
+ title=_("Is default?"), required=True, readonly=True,
+ description=_(
+ "Whether this base snap indicates the defaults used for snap "
+ "builds that do not specify a base snap.")))
+
+
+class IBaseSnapEditableAttributes(Interface):
+ """`IBaseSnap` attributes that can be edited.
+
+ Anyone can view these attributes, but they need launchpad.Edit to change.
+ """
+
+ name = exported(BaseSnapNameField(
+ title=_("Name"), required=True, readonly=False,
+ constraint=name_validator))
+
+ display_name = exported(TextLine(
+ title=_("Display name"), required=True, readonly=False))
+
+ title = Title(title=_("Title"), required=True, readonly=True)
+
+ distro_series = exported(Reference(
+ IDistroSeries, title=_("Distro series"),
+ required=True, readonly=False))
+
+ channels = exported(Dict(
+ title=_("Source snap channels"),
+ key_type=TextLine(), required=True, readonly=False,
+ description=_(
+ "A dictionary mapping snap names to channels to use when building "
+ "snaps that specify this base snap.")))
+
+
+class IBaseSnapEdit(Interface):
+ """`IBaseSnap` methods that require launchpad.Edit permission."""
+
+ def setDefault(value):
+ """Set whether this base snap is the default.
+
+ This is for internal use; the caller should ensure permission to
+ edit the base snap and should arrange to remove any existing default
+ first. Most callers should use `IBaseSnapSet.setDefault` instead.
+
+ :param value: True if this base snap should be the default,
+ otherwise False.
+ """
+
+ @export_destructor_operation()
+ @operation_for_version("devel")
+ def destroySelf():
+ """Delete the specified base snap.
+
+ :raises CannotDeleteBaseSnap: if the base snap cannot be deleted.
+ """
+
+
+class IBaseSnap(IBaseSnapView, IBaseSnapEditableAttributes):
+ """A base snap."""
+
+ # XXX cjwatson 2019-01-28 bug=760849: "beta" is a lie to get WADL
+ # generation working. Individual attributes must set their version to
+ # "devel".
+ export_as_webservice_entry(as_of="beta")
+
+
+class IBaseSnapSetEdit(Interface):
+ """`IBaseSnapSet` methods that require launchpad.Edit permission."""
+
+ @call_with(registrant=REQUEST_USER)
+ @export_factory_operation(
+ IBaseSnap, ["name", "display_name", "distro_series", "channels"])
+ @operation_for_version("devel")
+ def new(registrant, name, display_name, distro_series, channels,
+ date_created=None):
+ """Create an `IBaseSnap`."""
+
+ @operation_parameters(
+ base_snap=Reference(
+ title=_("Base snap"), required=True, schema=IBaseSnap))
+ @export_write_operation()
+ @operation_for_version("devel")
+ def setDefault(base_snap):
+ """Set the default base snap.
+
+ This will be used to pick the default distro series for snap builds
+ that do not specify a base.
+
+ :param base_snap: An `IBaseSnap`, or None to unset the default base
+ snap.
+ """
+
+
+class IBaseSnapSet(IBaseSnapSetEdit):
+ """Interface representing the set of base snaps."""
+
+ export_as_webservice_collection(IBaseSnap)
+
+ def __iter__():
+ """Iterate over `IBaseSnap`s."""
+
+ def __getitem__(name):
+ """Return the `IBaseSnap` with this name."""
+
+ @operation_parameters(
+ name=TextLine(title=_("Base snap name"), required=True))
+ @operation_returns_entry(IBaseSnap)
+ @export_read_operation()
+ @operation_for_version("devel")
+ def getByName(name):
+ """Return the `IBaseSnap` with this name.
+
+ :raises NoSuchBaseSnap: if no base snap exists with this name.
+ """
+
+ @operation_returns_entry(IBaseSnap)
+ @export_read_operation()
+ @operation_for_version("devel")
+ def getDefault():
+ """Get the default base snap.
+
+ This will be used to pick the default distro series for snap builds
+ that do not specify a base.
+ """
+
+ @collection_default_content()
+ def getAll():
+ """Return all `IBaseSnap`s."""
=== modified file 'lib/lp/snappy/interfaces/webservice.py'
--- lib/lp/snappy/interfaces/webservice.py 2018-06-15 13:21:14 +0000
+++ lib/lp/snappy/interfaces/webservice.py 2019-02-05 12:19:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""All the interfaces that are exposed through the webservice.
@@ -10,6 +10,8 @@
"""
__all__ = [
+ 'IBaseSnap',
+ 'IBaseSnapSet',
'ISnap',
'ISnapBuild',
'ISnapBuildRequest',
@@ -24,6 +26,10 @@
patch_entry_return_type,
patch_reference_property,
)
+from lp.snappy.interfaces.basesnap import (
+ IBaseSnap,
+ IBaseSnapSet,
+ )
from lp.snappy.interfaces.snap import (
ISnap,
ISnapBuildRequest,
=== added file 'lib/lp/snappy/model/basesnap.py'
--- lib/lp/snappy/model/basesnap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/basesnap.py 2019-02-05 12:19:08 +0000
@@ -0,0 +1,147 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Base snaps."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ "BaseSnap",
+ ]
+
+import pytz
+from storm.locals import (
+ Bool,
+ DateTime,
+ Int,
+ JSON,
+ Reference,
+ Store,
+ Storm,
+ Unicode,
+ )
+from zope.component import getUtility
+from zope.interface import implementer
+
+from lp.services.database.constants import DEFAULT
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+from lp.snappy.interfaces.basesnap import (
+ BaseSnapDefaultConflict,
+ CannotDeleteBaseSnap,
+ IBaseSnap,
+ IBaseSnapSet,
+ NoSuchBaseSnap,
+ )
+
+
+@implementer(IBaseSnap)
+class BaseSnap(Storm):
+ """See `IBaseSnap`."""
+
+ __storm_table__ = "BaseSnap"
+
+ id = Int(primary=True)
+
+ date_created = DateTime(
+ name="date_created", tzinfo=pytz.UTC, allow_none=False)
+
+ registrant_id = Int(name="registrant", allow_none=False)
+ registrant = Reference(registrant_id, "Person.id")
+
+ name = Unicode(name="name", allow_none=False)
+
+ display_name = Unicode(name="display_name", allow_none=False)
+
+ distro_series_id = Int(name="distro_series", allow_none=False)
+ distro_series = Reference(distro_series_id, "DistroSeries.id")
+
+ channels = JSON(name="channels", allow_none=False)
+
+ is_default = Bool(name="is_default", allow_none=False)
+
+ def __init__(self, registrant, name, display_name, distro_series, channels,
+ date_created=DEFAULT):
+ super(BaseSnap, self).__init__()
+ self.registrant = registrant
+ self.name = name
+ self.display_name = display_name
+ self.distro_series = distro_series
+ self.channels = channels
+ self.date_created = date_created
+ self.is_default = False
+
+ @property
+ def title(self):
+ """See `IBaseSnap`."""
+ return self.display_name
+
+ def setDefault(self, value):
+ """See `IBaseSnap`."""
+ if value:
+ # Check for an existing default.
+ existing = getUtility(IBaseSnapSet).getDefault()
+ if existing is not None and existing != self:
+ raise BaseSnapDefaultConflict(
+ "The default base snap is already set to %s." %
+ existing.name)
+ self.is_default = value
+
+ def destroySelf(self):
+ """See `IBaseSnap`."""
+ # Guard against unfortunate accidents.
+ if self.is_default:
+ raise CannotDeleteBaseSnap("Cannot delete the default base snap.")
+ Store.of(self).remove(self)
+
+
+@implementer(IBaseSnapSet)
+class BaseSnapSet:
+ """See `IBaseSnapSet`."""
+
+ def new(self, registrant, name, display_name, distro_series, channels,
+ date_created=DEFAULT):
+ """See `IBaseSnapSet`."""
+ store = IMasterStore(BaseSnap)
+ base_snap = BaseSnap(
+ registrant, name, display_name, distro_series, channels,
+ date_created=date_created)
+ store.add(base_snap)
+ return base_snap
+
+ def __iter__(self):
+ """See `IBaseSnapSet`."""
+ return iter(self.getAll())
+
+ def __getitem__(self, name):
+ """See `IBaseSnapSet`."""
+ return self.getByName(name)
+
+ def getByName(self, name):
+ """See `IBaseSnapSet`."""
+ base_snap = IStore(BaseSnap).find(
+ BaseSnap, BaseSnap.name == name).one()
+ if base_snap is None:
+ raise NoSuchBaseSnap(name)
+ return base_snap
+
+ def getDefault(self):
+ """See `IBaseSnapSet`."""
+ return IStore(BaseSnap).find(
+ BaseSnap, BaseSnap.is_default == True).one()
+
+ def setDefault(self, base_snap):
+ """See `IBaseSnapSet`."""
+ previous = self.getDefault()
+ if previous != base_snap:
+ if previous is not None:
+ previous.setDefault(False)
+ if base_snap is not None:
+ base_snap.setDefault(True)
+
+ def getAll(self):
+ """See `IBaseSnapSet`."""
+ return IStore(BaseSnap).find(BaseSnap).order_by(BaseSnap.name)
=== added file 'lib/lp/snappy/tests/test_basesnap.py'
--- lib/lp/snappy/tests/test_basesnap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_basesnap.py 2019-02-05 12:19:08 +0000
@@ -0,0 +1,274 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test base snaps."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from testtools.matchers import (
+ ContainsDict,
+ Equals,
+ Is,
+ )
+from zope.component import (
+ getAdapter,
+ getUtility,
+ )
+
+from lp.app.interfaces.security import IAuthorization
+from lp.services.webapp.interfaces import OAuthPermission
+from lp.snappy.interfaces.basesnap import (
+ CannotDeleteBaseSnap,
+ IBaseSnap,
+ IBaseSnapSet,
+ NoSuchBaseSnap,
+ )
+from lp.testing import (
+ api_url,
+ celebrity_logged_in,
+ logout,
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ ZopelessDatabaseLayer,
+ )
+from lp.testing.pages import webservice_for_person
+
+
+class TestBaseSnap(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_implements_interface(self):
+ # BaseSnap implements IBaseSnap.
+ base_snap = self.factory.makeBaseSnap()
+ self.assertProvides(base_snap, IBaseSnap)
+
+ def test_new_not_default(self):
+ base_snap = self.factory.makeBaseSnap()
+ self.assertFalse(base_snap.is_default)
+
+ def test_anonymous(self):
+ # Anyone can view an `IBaseSnap`.
+ base_snap = self.factory.makeBaseSnap()
+ authz = getAdapter(base_snap, IAuthorization, name="launchpad.View")
+ self.assertTrue(authz.checkUnauthenticated())
+
+ def test_destroySelf(self):
+ base_snap = self.factory.makeBaseSnap()
+ base_snap_name = base_snap.name
+ base_snap_set = getUtility(IBaseSnapSet)
+ self.assertEqual(base_snap, base_snap_set.getByName(base_snap_name))
+ base_snap.destroySelf()
+ self.assertRaises(
+ NoSuchBaseSnap, base_snap_set.getByName, base_snap_name)
+
+ def test_destroySelf_refuses_default(self):
+ base_snap = self.factory.makeBaseSnap()
+ getUtility(IBaseSnapSet).setDefault(base_snap)
+ self.assertRaises(CannotDeleteBaseSnap, base_snap.destroySelf)
+
+
+class TestBaseSnapSet(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_getByName(self):
+ base_snap_set = getUtility(IBaseSnapSet)
+ base_snap = self.factory.makeBaseSnap(name="foo")
+ self.factory.makeBaseSnap()
+ self.assertEqual(base_snap, base_snap_set.getByName("foo"))
+ self.assertRaises(NoSuchBaseSnap, base_snap_set.getByName, "bar")
+
+ def test_getDefault(self):
+ base_snap_set = getUtility(IBaseSnapSet)
+ base_snap = self.factory.makeBaseSnap()
+ self.factory.makeBaseSnap()
+ self.assertIsNone(base_snap_set.getDefault())
+ base_snap_set.setDefault(base_snap)
+ self.assertEqual(base_snap, base_snap_set.getDefault())
+
+ def test_setDefault(self):
+ base_snap_set = getUtility(IBaseSnapSet)
+ base_snaps = [self.factory.makeBaseSnap() for _ in range(3)]
+ base_snap_set.setDefault(base_snaps[0])
+ self.assertEqual(
+ [True, False, False],
+ [base_snap.is_default for base_snap in base_snaps])
+ base_snap_set.setDefault(base_snaps[1])
+ self.assertEqual(
+ [False, True, False],
+ [base_snap.is_default for base_snap in base_snaps])
+ base_snap_set.setDefault(None)
+ self.assertEqual(
+ [False, False, False],
+ [base_snap.is_default for base_snap in base_snaps])
+
+ def test_getAll(self):
+ base_snaps = [self.factory.makeBaseSnap() for _ in range(3)]
+ self.assertContentEqual(base_snaps, getUtility(IBaseSnapSet).getAll())
+
+
+class TestBaseSnapWebservice(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_new_unpriv(self):
+ # An unprivileged user cannot create a BaseSnap.
+ person = self.factory.makePerson()
+ distroseries = self.factory.makeDistroSeries()
+ distroseries_url = api_url(distroseries)
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ response = webservice.named_post(
+ "/+base-snaps", "new",
+ name="dummy", display_name="Dummy",
+ distro_series=distroseries_url, channels={"snapcraft": "stable"})
+ self.assertEqual(401, response.status)
+
+ def test_new(self):
+ # A registry expert can create a BaseSnap.
+ person = self.factory.makeRegistryExpert()
+ distroseries = self.factory.makeDistroSeries()
+ distroseries_url = api_url(distroseries)
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ logout()
+ response = webservice.named_post(
+ "/+base-snaps", "new",
+ name="dummy", display_name="Dummy",
+ distro_series=distroseries_url, channels={"snapcraft": "stable"})
+ self.assertEqual(201, response.status)
+ base_snap = webservice.get(response.getHeader("Location")).jsonBody()
+ with person_logged_in(person):
+ self.assertThat(base_snap, ContainsDict({
+ "registrant_link": Equals(
+ webservice.getAbsoluteUrl(api_url(person))),
+ "name": Equals("dummy"),
+ "display_name": Equals("Dummy"),
+ "distro_series_link": Equals(
+ webservice.getAbsoluteUrl(distroseries_url)),
+ "channels": Equals({"snapcraft": "stable"}),
+ "is_default": Is(False),
+ }))
+
+ def test_new_duplicate_name(self):
+ # An attempt to create a BaseSnap with a duplicate name is rejected.
+ person = self.factory.makeRegistryExpert()
+ distroseries = self.factory.makeDistroSeries()
+ distroseries_url = api_url(distroseries)
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ logout()
+ response = webservice.named_post(
+ "/+base-snaps", "new",
+ name="dummy", display_name="Dummy",
+ distro_series=distroseries_url, channels={"snapcraft": "stable"})
+ self.assertEqual(201, response.status)
+ response = webservice.named_post(
+ "/+base-snaps", "new",
+ name="dummy", display_name="Dummy",
+ distro_series=distroseries_url, channels={"snapcraft": "stable"})
+ self.assertEqual(400, response.status)
+ self.assertEqual(
+ "name: dummy is already in use by another base snap.",
+ response.body)
+
+ def test_getByName(self):
+ # lp.base_snaps.getByName returns a matching BaseSnap.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ with celebrity_logged_in("registry_experts"):
+ self.factory.makeBaseSnap(name="dummy")
+ response = webservice.named_get(
+ "/+base-snaps", "getByName", name="dummy")
+ self.assertEqual(200, response.status)
+ self.assertEqual("dummy", response.jsonBody()["name"])
+
+ def test_getByName_missing(self):
+ # lp.base_snaps.getByName returns 404 for a non-existent BaseSnap.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ logout()
+ response = webservice.named_get(
+ "/+base-snaps", "getByName", name="nonexistent")
+ self.assertEqual(404, response.status)
+ self.assertEqual("No such base snap: 'nonexistent'.", response.body)
+
+ def test_getDefault(self):
+ # lp.base_snaps.getDefault returns the default BaseSnap, if any.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ response = webservice.named_get("/+base-snaps", "getDefault")
+ self.assertEqual(200, response.status)
+ self.assertIsNone(response.jsonBody())
+ with celebrity_logged_in("registry_experts"):
+ getUtility(IBaseSnapSet).setDefault(
+ self.factory.makeBaseSnap(name="default-base"))
+ self.factory.makeBaseSnap()
+ response = webservice.named_get("/+base-snaps", "getDefault")
+ self.assertEqual(200, response.status)
+ self.assertEqual("default-base", response.jsonBody()["name"])
+
+ def test_setDefault_unpriv(self):
+ # An unprivileged user cannot set the default BaseSnap.
+ person = self.factory.makePerson()
+ with celebrity_logged_in("registry_experts"):
+ base_snap = self.factory.makeBaseSnap()
+ base_snap_url = api_url(base_snap)
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ response = webservice.named_post(
+ "/+base-snaps", "setDefault", base_snap=base_snap_url)
+ self.assertEqual(401, response.status)
+
+ def test_setDefault(self):
+ # A registry expert can set the default BaseSnap.
+ person = self.factory.makeRegistryExpert()
+ with person_logged_in(person):
+ base_snaps = [self.factory.makeBaseSnap() for _ in range(3)]
+ base_snap_urls = [api_url(base_snap) for base_snap in base_snaps]
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ response = webservice.named_post(
+ "/+base-snaps", "setDefault", base_snap=base_snap_urls[0])
+ self.assertEqual(200, response.status)
+ with person_logged_in(person):
+ self.assertEqual(
+ base_snaps[0], getUtility(IBaseSnapSet).getDefault())
+ response = webservice.named_post(
+ "/+base-snaps", "setDefault", base_snap=base_snap_urls[1])
+ self.assertEqual(200, response.status)
+ with person_logged_in(person):
+ self.assertEqual(
+ base_snaps[1], getUtility(IBaseSnapSet).getDefault())
+
+ def test_collection(self):
+ # lp.base_snaps is a collection of all BaseSnaps.
+ person = self.factory.makePerson()
+ webservice = webservice_for_person(
+ person, permission=OAuthPermission.READ_PUBLIC)
+ webservice.default_api_version = "devel"
+ with celebrity_logged_in("registry_experts"):
+ for i in range(3):
+ self.factory.makeBaseSnap(name="base-%d" % i)
+ response = webservice.get("/+base-snaps")
+ self.assertEqual(200, response.status)
+ self.assertContentEqual(
+ ["base-0", "base-1", "base-2"],
+ [entry["name"] for entry in response.jsonBody()["entries"]])
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2018-12-12 10:37:06 +0000
+++ lib/lp/testing/factory.py 2019-02-05 12:19:08 +0000
@@ -2,7 +2,7 @@
# NOTE: The first line above must stay first; do not move the copyright
# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
#
-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Testing infrastructure for the Launchpad application.
@@ -286,6 +286,7 @@
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.worlddata.interfaces.country import ICountrySet
from lp.services.worlddata.interfaces.language import ILanguageSet
+from lp.snappy.interfaces.basesnap import IBaseSnapSet
from lp.snappy.interfaces.snap import ISnapSet
from lp.snappy.interfaces.snapbuild import ISnapBuildSet
from lp.snappy.interfaces.snappyseries import ISnappySeriesSet
@@ -4843,6 +4844,24 @@
IStore(snappy_series).flush()
return snappy_series
+ def makeBaseSnap(self, registrant=None, name=None, display_name=None,
+ distro_series=None, channels=None, date_created=DEFAULT):
+ """Make a new BaseSnap."""
+ if registrant is None:
+ registrant = self.makePerson()
+ if name is None:
+ name = self.getUniqueString(u"base-snap-name")
+ if display_name is None:
+ display_name = SPACE.join(
+ word.capitalize() for word in name.split('-'))
+ if distro_series is None:
+ distro_series = self.makeDistroSeries()
+ if channels is None:
+ channels = {u"snapcraft": u"stable"}
+ return getUtility(IBaseSnapSet).new(
+ registrant, name, display_name, distro_series, channels,
+ date_created=date_created)
+
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by
Follow ups