← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/specifications-sharing-service into lp:launchpad

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/specifications-sharing-service into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/specifications-sharing-service/+merge/124978

This branch updates several methods of the class SharingService so that
they can deal with specifications in addition to bugs and branches.

changed methods:

    getSharedArtifacts()
    getVisibleArtifacts()

new method:

    getSharedSpecifications()

The diff contains some "noise" because some changed methods returned
a tuples (bugs, branches) before this change, and now return a tuple
(bugs, branches, specifications). This requires of course changes in
all callsites.

The method getVisibleArtifacts() uses IBugTaskSet.search() to find
"the right" bugs, and IAllBranches to find "the right" branches; we
do not have yet anything similar for specs, so I iimpkemented the
SQL query for the permission check directly in the class SharingService.

I am a bit concerned about the performance of the SQL query, but
it will for now only be used in a test: my main goal right now is to
make it possible to revoke access grants for specifications, and
I need the changes of getVisibleArtifacts() for a test of
SharingService.revokeAccessGrants().

test:

./bin/test -vvt lp.registry.services.tests.test_sharingservice

no lint

-- 
https://code.launchpad.net/~adeuring/launchpad/specifications-sharing-service/+merge/124978
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/specifications-sharing-service into lp:launchpad.
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2012-09-07 20:25:51 +0000
+++ lib/lp/bugs/model/bug.py	2012-09-18 16:33:28 +0000
@@ -811,7 +811,7 @@
         # there is at least one bugtask for which access can be checked.
         if self.default_bugtask:
             service = getUtility(IService, 'sharing')
-            bugs, ignored = service.getVisibleArtifacts(
+            bugs, ignored, ignored = service.getVisibleArtifacts(
                 person, bugs=[self], ignore_permissions=True)
             if not bugs:
                 service.ensureAccessGrants(

=== modified file 'lib/lp/code/browser/branchsubscription.py'
--- lib/lp/code/browser/branchsubscription.py	2012-09-05 21:51:06 +0000
+++ lib/lp/code/browser/branchsubscription.py	2012-09-18 16:33:28 +0000
@@ -294,7 +294,7 @@
         url = canonical_url(self.branch)
         # If the subscriber can no longer see the branch, redirect them away.
         service = getUtility(IService, 'sharing')
-        ignored, branches = service.getVisibleArtifacts(
+        ignored, branches, ignored = service.getVisibleArtifacts(
             self.person, branches=[self.branch], ignore_permissions=True)
         if not branches:
             url = canonical_url(self.branch.target)

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2012-09-18 04:13:42 +0000
+++ lib/lp/code/model/branch.py	2012-09-18 16:33:28 +0000
@@ -896,7 +896,7 @@
             subscription.review_level = code_review_level
         # Grant the subscriber access if they can't see the branch.
         service = getUtility(IService, 'sharing')
-        ignored, branches = service.getVisibleArtifacts(
+        ignored, branches, ignored = service.getVisibleArtifacts(
             person, branches=[self], ignore_permissions=True)
         if not branches:
             service.ensureAccessGrants(

=== modified file 'lib/lp/code/model/tests/test_branchsubscription.py'
--- lib/lp/code/model/tests/test_branchsubscription.py	2012-07-18 10:44:24 +0000
+++ lib/lp/code/model/tests/test_branchsubscription.py	2012-09-18 16:33:28 +0000
@@ -133,7 +133,7 @@
                 None, CodeReviewNotificationLevel.NOEMAIL, owner)
             # The stacked on branch should be visible.
             service = getUtility(IService, 'sharing')
-            ignored, visible_branches = service.getVisibleArtifacts(
+            ignored, visible_branches, ignored = service.getVisibleArtifacts(
                 grantee, branches=[private_stacked_on_branch])
             self.assertContentEqual(
                 [private_stacked_on_branch], visible_branches)
@@ -161,7 +161,7 @@
                 grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
                 None, CodeReviewNotificationLevel.NOEMAIL, owner)
             # The stacked on branch should not be visible.
-            ignored, visible_branches = service.getVisibleArtifacts(
+            ignored, visible_branches, ignored = service.getVisibleArtifacts(
                 grantee, branches=[private_stacked_on_branch])
             self.assertContentEqual([], visible_branches)
             self.assertIn(

=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2012-09-14 01:03:41 +0000
+++ lib/lp/registry/browser/pillar.py	2012-09-18 16:33:28 +0000
@@ -421,7 +421,7 @@
     def _loadSharedArtifacts(self):
         # As a concrete can by linked via more than one policy, we use sets to
         # filter out dupes.
-        self.bugtasks, self.branches = (
+        self.bugtasks, self.branches, self.specifications = (
             self.sharing_service.getSharedArtifacts(
                 self.pillar, self.person, self.user))
         bug_ids = set([bugtask.bug.id for bugtask in self.bugtasks])

=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
--- lib/lp/registry/interfaces/sharingservice.py	2012-09-16 12:49:11 +0000
+++ lib/lp/registry/interfaces/sharingservice.py	2012-09-18 16:33:28 +0000
@@ -79,7 +79,7 @@
 
         :param user: the user making the request. Only artifacts visible to the
              user will be included in the result.
-        :return: a (bugtasks, branches) tuple
+        :return: a (bugtasks, branches, specifications) tuple
         """
 
     @export_read_operation()
@@ -116,6 +116,21 @@
         :return: a collection of branches
         """
 
+    @export_read_operation()
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(
+        pillar=Reference(IPillar, title=_('Pillar'), required=True),
+        person=Reference(IPerson, title=_('Person'), required=True))
+    @operation_returns_collection_of(IBranch)
+    @operation_for_version('devel')
+    def getSharedSpecifications(pillar, person, user):
+        """Return the specifications shared between the pillar and person.
+
+        :param user: the user making the request. Only branches visible to the
+             user will be included in the result.
+        :return: a collection of branches
+        """
+
     def getVisibleArtifacts(person, branches=None, bugs=None):
         """Return the artifacts shared with person.
 

=== modified file 'lib/lp/registry/services/sharingservice.py'
--- lib/lp/registry/services/sharingservice.py	2012-09-14 13:25:18 +0000
+++ lib/lp/registry/services/sharingservice.py	2012-09-18 16:33:28 +0000
@@ -17,15 +17,18 @@
     Count,
     In,
     Join,
+    LeftJoin,
     Or,
     Select,
     )
+from storm.store import Store
 from zope.component import getUtility
 from zope.interface import implements
 from zope.security.interfaces import Unauthorized
 from zope.traversing.browser.absoluteurl import absoluteURL
 
 from lp.app.browser.tales import ObjectImageDisplayAPI
+from lp.blueprints.model.specification import Specification
 from lp.bugs.interfaces.bugtask import IBugTaskSet
 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
 from lp.code.interfaces.branchcollection import IAllBranches
@@ -51,13 +54,16 @@
     )
 from lp.registry.interfaces.sharingservice import ISharingService
 from lp.registry.model.accesspolicy import (
+    AccessArtifact,
     AccessArtifactGrant,
     AccessPolicy,
     AccessPolicyArtifact,
     AccessPolicyGrant,
+    AccessPolicyGrantFlat,
     )
 from lp.registry.model.person import Person
 from lp.registry.model.teammembership import TeamParticipation
+from lp.services.database.bulk import load
 from lp.services.database.lpstorm import IStore
 from lp.services.database.stormexpr import ColumnSelect
 from lp.services.searchbuilder import any
@@ -115,17 +121,20 @@
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedArtifacts(self, pillar, person, user, include_bugs=True,
-                           include_branches=True):
+                           include_branches=True, include_specifications=True):
         """See `ISharingService`."""
         policies = getUtility(IAccessPolicySource).findByPillar([pillar])
         flat_source = getUtility(IAccessPolicyGrantFlatSource)
         bug_ids = set()
         branch_ids = set()
+        specification_ids = set()
         for artifact in flat_source.findArtifactsByGrantee(person, policies):
             if artifact.bug_id and include_bugs:
                 bug_ids.add(artifact.bug_id)
             elif artifact.branch_id and include_branches:
                 branch_ids.add(artifact.branch_id)
+            elif artifact.specification_id and include_specifications:
+                specification_ids.add(artifact.specification_id)
 
         # Load the bugs.
         bugtasks = []
@@ -140,25 +149,72 @@
             wanted_branches = all_branches.visibleByUser(user).withIds(
                 *branch_ids)
             branches = list(wanted_branches.getBranches())
+        specifications = []
+        if specification_ids:
+            specifications = load(Specification, specification_ids)
 
-        return bugtasks, branches
+        return bugtasks, branches, specifications
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedBugs(self, pillar, person, user):
         """See `ISharingService`."""
-        bugtasks, ignore = self.getSharedArtifacts(
-            pillar, person, user, include_branches=False)
+        bugtasks, ignore, ignore = self.getSharedArtifacts(
+            pillar, person, user, include_branches=False,
+            include_specifications=False)
         return bugtasks
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedBranches(self, pillar, person, user):
         """See `ISharingService`."""
-        ignore, branches = self.getSharedArtifacts(
-            pillar, person, user, include_bugs=False)
+        ignore, branches, ignore = self.getSharedArtifacts(
+            pillar, person, user, include_bugs=False,
+            include_specifications=False)
         return branches
 
+    @available_with_permission('launchpad.Driver', 'pillar')
+    def getSharedSpecifications(self, pillar, person, user):
+        """See `ISharingService`."""
+        ignore, ignore, specifications = self.getSharedArtifacts(
+            pillar, person, user, include_bugs=False,
+            include_branches=False)
+        return specifications
+
+    def _getVisiblePrivateSpecificationIDs(self, person, specifications):
+        store = Store.of(specifications[0])
+        tables = (
+            Specification,
+            Join(
+                AccessPolicy,
+                And(
+                    Or(
+                        Specification.distributionID ==
+                            AccessPolicy.distribution_id,
+                        Specification.productID ==
+                            AccessPolicy.product_id),
+                    AccessPolicy.type == Specification.information_type)),
+            Join(
+                AccessPolicyGrantFlat,
+                AccessPolicy.id == AccessPolicyGrantFlat.policy_id
+                ),
+            LeftJoin(
+                AccessArtifact,
+                AccessArtifact.id ==
+                    AccessPolicyGrantFlat.abstract_artifact_id),
+            Join(
+                TeamParticipation,
+                TeamParticipation.teamID ==
+                    AccessPolicyGrantFlat.grantee_id))
+        spec_ids = [spec.id for spec in specifications]
+        return set(store.using(*tables).find(
+            Specification.id,
+            Or(
+                AccessPolicyGrantFlat.abstract_artifact_id == None,
+                AccessArtifact.specification == Specification.id),
+            TeamParticipation.personID == person.id,
+            In(Specification.id, spec_ids)))
+
     def getVisibleArtifacts(self, person, branches=None, bugs=None,
-                            ignore_permissions=False):
+                            specifications=None, ignore_permissions=False):
         """See `ISharingService`."""
         bugs_by_id = {}
         branches_by_id = {}
@@ -172,6 +228,10 @@
                 and not check_permission('launchpad.View', branch)):
                 raise Unauthorized
             branches_by_id[branch.id] = branch
+        for spec in specifications or []:
+            if (not ignore_permissions
+                and not check_permission('launchpad.View', spec)):
+                raise Unauthorized
 
         # Load the bugs.
         visible_bug_ids = []
@@ -189,7 +249,15 @@
                 *branches_by_id.keys())
             visible_branches = list(wanted_branches.getBranches())
 
-        return visible_bugs, visible_branches
+        visible_specs = []
+        if specifications:
+            visible_private_spec_ids = self._getVisiblePrivateSpecificationIDs(
+                person, specifications)
+            visible_specs = [
+                spec for spec in specifications
+                if spec.id in visible_private_spec_ids or not spec.private]
+
+        return visible_bugs, visible_branches, visible_specs
 
     def getInvisibleArtifacts(self, person, branches=None, bugs=None):
         """See `ISharingService`."""

=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py	2012-09-16 12:49:11 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py	2012-09-18 16:33:28 +0000
@@ -980,8 +980,8 @@
 
         # Check that grantees have expected access grants and subscriptions.
         for person in [team_grantee, person_grantee]:
-            visible_bugs, visible_branches = self.service.getVisibleArtifacts(
-                person, branches, bugs)
+            visible_bugs, visible_branches, visible_specs = (
+                self.service.getVisibleArtifacts(person, branches, bugs))
             self.assertContentEqual(bugs or [], visible_bugs)
             self.assertContentEqual(branches or [], visible_branches)
         for person in [team_grantee, person_grantee]:
@@ -1004,8 +1004,8 @@
         for person in [team_grantee, person_grantee]:
             for bug in bugs or []:
                 self.assertNotIn(person, bug.getDirectSubscribers())
-            visible_bugs, visible_branches = self.service.getVisibleArtifacts(
-                person, branches, bugs)
+            visible_bugs, visible_branches, visible_specs = (
+                self.service.getVisibleArtifacts(person, branches, bugs))
             self.assertContentEqual([], visible_bugs)
             self.assertContentEqual([], visible_branches)
 
@@ -1120,8 +1120,8 @@
         owner = self.factory.makePerson()
         product = self.factory.makeProduct(
             owner=owner,
-            specification_sharing_policy=
-                SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY)
+            specification_sharing_policy=(
+                SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
         login_person(owner)
         specification = self.factory.makeSpecification(
             product=product, owner=owner)
@@ -1232,6 +1232,12 @@
                 product=product, owner=product.owner,
                 information_type=InformationType.USERDATA)
             branches.append(branch)
+        specs = []
+        for x in range(0, 10):
+            spec = self.factory.makeSpecification(
+                product=product, owner=product.owner,
+                information_type=InformationType.PROPRIETARY)
+            specs.append(spec)
 
         # Grant access to grantee as well as the person who will be doing the
         # query. The person who will be doing the query is not granted access
@@ -1251,32 +1257,39 @@
             grant_access(bug, i == 9)
         for i, branch in enumerate(branches):
             grant_access(branch, i == 9)
-        return bug_tasks, branches
+        getUtility(IService, 'sharing').ensureAccessGrants(
+            [grantee], product.owner, specifications=specs[:9])
+        return bug_tasks, branches, specs
 
     def test_getSharedArtifacts(self):
         # Test the getSharedArtifacts method.
         owner = self.factory.makePerson()
-        product = self.factory.makeProduct(owner=owner)
+        product = self.factory.makeProduct(
+            owner=owner, specification_sharing_policy=(
+            SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        bug_tasks, branches = self.create_shared_artifacts(
+        bug_tasks, branches, specs = self.create_shared_artifacts(
             product, grantee, user)
 
         # Check the results.
-        shared_bugtasks, shared_branches = self.service.getSharedArtifacts(
-            product, grantee, user)
+        shared_bugtasks, shared_branches, shared_specs = (
+            self.service.getSharedArtifacts(product, grantee, user))
         self.assertContentEqual(bug_tasks[:9], shared_bugtasks)
         self.assertContentEqual(branches[:9], shared_branches)
+        self.assertContentEqual(specs[:9], shared_specs)
 
     def test_getSharedBugs(self):
         # Test the getSharedBugs method.
         owner = self.factory.makePerson()
-        product = self.factory.makeProduct(owner=owner)
+        product = self.factory.makeProduct(
+            owner=owner, specification_sharing_policy=(
+            SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        bug_tasks, ignored = self.create_shared_artifacts(
+        bug_tasks, ignored, ignored = self.create_shared_artifacts(
             product, grantee, user)
 
         # Check the results.
@@ -1286,11 +1299,13 @@
     def test_getSharedBranches(self):
         # Test the getSharedBranches method.
         owner = self.factory.makePerson()
-        product = self.factory.makeProduct(owner=owner)
+        product = self.factory.makeProduct(
+            owner=owner, specification_sharing_policy=(
+            SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        ignored, branches = self.create_shared_artifacts(
+        ignored, branches, ignored = self.create_shared_artifacts(
             product, grantee, user)
 
         # Check the results.
@@ -1298,6 +1313,23 @@
             product, grantee, user)
         self.assertContentEqual(branches[:9], shared_branches)
 
+    def test_getSharedSpecifications(self):
+        # Test the getSharedSpecifications method.
+        owner = self.factory.makePerson()
+        product = self.factory.makeProduct(
+            owner=owner, specification_sharing_policy=(
+            SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
+        login_person(owner)
+        grantee = self.factory.makePerson()
+        user = self.factory.makePerson()
+        ignored, ignored, specifications = self.create_shared_artifacts(
+            product, grantee, user)
+
+        # Check the results.
+        shared_specifications = self.service.getSharedSpecifications(
+            product, grantee, user)
+        self.assertContentEqual(specifications[:9], shared_specifications)
+
     def test_getPeopleWithAccessBugs(self):
         # Test the getPeopleWithoutAccess method with bugs.
         owner = self.factory.makePerson()
@@ -1354,7 +1386,10 @@
     def _make_Artifacts(self):
         # Make artifacts for test (in)visible artifact methods.
         owner = self.factory.makePerson()
-        product = self.factory.makeProduct(owner=owner)
+        product = self.factory.makeProduct(
+            owner=owner,
+            specification_sharing_policy=(
+                SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
         grantee = self.factory.makePerson()
         login_person(owner)
 
@@ -1371,6 +1406,13 @@
                 information_type=InformationType.USERDATA)
             branches.append(branch)
 
+        specifications = []
+        for x in range(0, 10):
+            spec = self.factory.makeSpecification(
+                product=product, owner=owner,
+                information_type=InformationType.PROPRIETARY)
+            specifications.append(spec)
+
         def grant_access(artifact):
             access_artifact = self.factory.makeAccessArtifact(
                 concrete=artifact)
@@ -1383,20 +1425,33 @@
             grant_access(bug)
         for branch in branches[:5]:
             grant_access(branch)
-        return grantee, branches, bugs
+        for spec in specifications[:5]:
+            grant_access(spec)
+        return grantee, owner, branches, bugs, specifications
 
     def test_getVisibleArtifacts(self):
         # Test the getVisibleArtifacts method.
-        grantee, branches, bugs = self._make_Artifacts()
+        grantee, ignore, branches, bugs, specs = self._make_Artifacts()
         # Check the results.
-        shared_bugs, shared_branches = self.service.getVisibleArtifacts(
-            grantee, branches, bugs)
+        shared_bugs, shared_branches, shared_specs = (
+            self.service.getVisibleArtifacts(grantee, branches, bugs, specs))
         self.assertContentEqual(bugs[:5], shared_bugs)
         self.assertContentEqual(branches[:5], shared_branches)
+        self.assertContentEqual(specs[:5], shared_specs)
+
+    def test_getVisibleArtifacts_grant_on_pillar(self):
+        # getVisibleArtifacts() returns private specifications if
+        # user has a policy grant for the pillar of the specification.
+        ignore, owner, branches, bugs, specs = self._make_Artifacts()
+        shared_bugs, shared_branches, shared_specs = (
+            self.service.getVisibleArtifacts(owner, branches, bugs, specs))
+        self.assertContentEqual(bugs, shared_bugs)
+        self.assertContentEqual(branches, shared_branches)
+        self.assertContentEqual(specs, shared_specs)
 
     def test_getInvisibleArtifacts(self):
         # Test the getInvisibleArtifacts method.
-        grantee, branches, bugs = self._make_Artifacts()
+        grantee, ignore, branches, bugs, specs = self._make_Artifacts()
         # Check the results.
         not_shared_bugs, not_shared_branches = (
             self.service.getInvisibleArtifacts(grantee, branches, bugs))
@@ -1423,16 +1478,16 @@
                 information_type=InformationType.USERDATA)
             bugs.append(bug)
 
-        shared_bugs, shared_branches = self.service.getVisibleArtifacts(
-            grantee, bugs=bugs)
+        shared_bugs, shared_branches, shared_specs = (
+            self.service.getVisibleArtifacts(grantee, bugs=bugs))
         self.assertContentEqual(bugs, shared_bugs)
 
         # Change some bugs.
         for x in range(0, 5):
             change_callback(bugs[x], owner)
         # Check the results.
-        shared_bugs, shared_branches = self.service.getVisibleArtifacts(
-            grantee, bugs=bugs)
+        shared_bugs, shared_branches, shared_specs = (
+            self.service.getVisibleArtifacts(grantee, bugs=bugs))
         self.assertContentEqual(bugs[5:], shared_bugs)
 
     def test_getVisibleArtifacts_bug_policy_change(self):
@@ -1519,7 +1574,9 @@
     def setUp(self):
         super(ApiTestMixin, self).setUp()
         self.owner = self.factory.makePerson(name='thundercat')
-        self.pillar = self.factory.makeProduct(owner=self.owner)
+        self.pillar = self.factory.makeProduct(
+            owner=self.owner, specification_sharing_policy=(
+            SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
         self.grantee = self.factory.makePerson(name='grantee')
         self.grantor = self.factory.makePerson()
         self.grantee_uri = canonical_url(self.grantee, force_local_path=True)
@@ -1530,11 +1587,16 @@
         self.branch = self.factory.makeBranch(
             owner=self.owner, product=self.pillar,
             information_type=InformationType.PRIVATESECURITY)
+        self.spec = self.factory.makeSpecification(
+            product=self.pillar, owner=self.owner,
+            information_type=InformationType.PROPRIETARY)
         login_person(self.owner)
         self.bug.subscribe(self.grantee, self.owner)
         self.branch.subscribe(
             self.grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
             None, CodeReviewNotificationLevel.NOEMAIL, self.owner)
+        getUtility(IService, 'sharing').ensureAccessGrants(
+            [self.grantee], self.grantor, specifications=[self.spec])
         transaction.commit()
 
     def test_getPillarGranteeData(self):
@@ -1546,7 +1608,8 @@
         self.assertEqual(
             {InformationType.USERDATA.name: SharingPermission.ALL.name,
              InformationType.PRIVATESECURITY.name:
-                 SharingPermission.SOME.name},
+                 SharingPermission.SOME.name,
+             InformationType.PROPRIETARY.name: SharingPermission.SOME.name},
             grantee_data['permissions'])
 
 
@@ -1628,7 +1691,7 @@
         self.assertEqual(bugtasks[0].title, self.bug.default_bugtask.title)
 
     def test_getSharedBranches(self):
-        # Test the exported getSharedArtifacts() method.
+        # Test the exported getSharedBranches() method.
         ws_pillar = ws_object(self.launchpad, self.pillar)
         ws_grantee = ws_object(self.launchpad, self.grantee)
         branches = self.service.getSharedBranches(
@@ -1636,13 +1699,23 @@
         self.assertEqual(1, len(branches))
         self.assertEqual(branches[0].unique_name, self.branch.unique_name)
 
+    def test_getSharedSpecifications(self):
+        # Test the exported getSharedSpecifications() method.
+        ws_pillar = ws_object(self.launchpad, self.pillar)
+        ws_grantee = ws_object(self.launchpad, self.grantee)
+        specifications = self.service.getSharedSpecifications(
+            pillar=ws_pillar, person=ws_grantee)
+        self.assertEqual(1, len(specifications))
+        self.assertEqual(specifications[0].name, self.spec.name)
+
     def test_getSharedArtifacts(self):
         # Test the exported getSharedArtifacts() method.
         ws_pillar = ws_object(self.launchpad, self.pillar)
         ws_grantee = ws_object(self.launchpad, self.grantee)
-        (bugtasks, branches) = self.service.getSharedArtifacts(
+        (bugtasks, branches, specs) = self.service.getSharedArtifacts(
             pillar=ws_pillar, person=ws_grantee)
         self.assertEqual(1, len(bugtasks))
         self.assertEqual(1, len(branches))
+        self.assertEqual(1, len(specs))
         self.assertEqual(bugtasks[0]['title'], self.bug.default_bugtask.title)
         self.assertEqual(branches[0]['unique_name'], self.branch.unique_name)


Follow ups