Merge lp:~adeuring/launchpad/sharingjob-remove-bp-subscriptions into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Abel Deuring
Approved revision: no longer in the source branch.
Merged at revision: 16015
Proposed branch: lp:~adeuring/launchpad/sharingjob-remove-bp-subscriptions
Merge into: lp:launchpad
Diff against target: 616 lines (+299/-87)
5 files modified
database/schema/security.cfg (+2/-0)
lib/lp/blueprints/model/specification.py (+57/-2)
lib/lp/blueprints/tests/test_specification.py (+52/-1)
lib/lp/registry/model/sharingjob.py (+53/-0)
lib/lp/registry/tests/test_sharingjob.py (+135/-84)
To merge this branch: bzr merge lp:~adeuring/launchpad/sharingjob-remove-bp-subscriptions
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+126008@code.launchpad.net

Commit message

specifications added to RemoveArtifactSubscriptionsJob.

Description of the change

This branch extends RemoveArtifactSubscriptionsJob so that it
removes subscriptions to specifications if a subscriber does no longer
have grants to access these specifications.

The change in lib/lp/registry/model/sharingjob.py is mostly
a mechanical change that adds specification handling to bug and branch
handling.

The method issues a Storm query to find subscribers of a specification
without access grants. The Storm expression "person with[out] access
to a spec" is defined in a new function get_specification_privacy_filter(),
similar to the existing functions get_bug_privacy_filter() and
get_branch_privacy_filter().

RemoveArtifactSubscriptionsJob accepted not only bugs and branches but
arbitrary objects, which I found a bit confusing while working on this
branch, so I added a check to ensure that only those objects can be
passed to this object that the class can handle, i.e. bugs, branches
and blueprints.

Many tests of RemoveArtifactSubscriptionsJob rely on two methods
_assert_bug_change_unsubscribes() and _assert_branch_change_unsubscribes()
which were quite long but nearly identical. The new test
test_change_information_type_specification() requires a third
variant of these methods, _assert_specification_change_unsubscribes().

I refactored all three _assert.* methods to use a common method.

tests:

./bin/test -vvt lp.registry.tests.test_sharingjob
./bin/test -vvt lp.blueprints.tests.test_specification.*test_get_specification_privacy_filter

no lint

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. Here are some minor comments:

It seems you have an extra underscore in the test name
"test_create__bad_artifact_class".

I like the generalization of _assert_artifact_change_unsubscribes. It is
unfortunate that the function is so long though. I wonder if instead of
passing in a test type we could instead break out the contents of that if block
into functions that are instead passed in and called.

Another idea: break the _assert_artifact_change_unsubscribes into two
functions, one for the code before "if test_type" and one after. The
individual assertions could then call the first function, then include their
unique code (currently contained in the if) and then finally call the last
function.

review: Approve (code)
Revision history for this message
Abel Deuring (adeuring) wrote :

Benji, thanks for these suggestions. I chose the first variant, moved the "if test_type == " blocks into the callsites. _assert_artifact_change_unsubscribes() is still quite long, but at least a bit more readable...

Revision history for this message
Benji York (benji) wrote :

On Mon, Sep 24, 2012 at 2:16 PM, Abel Deuring
<email address hidden> wrote:
> Benji, thanks for these suggestions. I chose the first variant, moved the "if
> test_type == " blocks into the callsites.
> _assert_artifact_change_unsubscribes() is still quite long, but at least a
> bit more readable...

Cool.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2012-09-21 00:08:41 +0000
3+++ database/schema/security.cfg 2012-09-24 14:20:26 +0000
4@@ -1966,6 +1966,8 @@
5 public.person = SELECT
6 public.product = SELECT
7 public.sharingjob = SELECT, INSERT, UPDATE
8+public.specification = SELECT
9+public.specificationsubscription = SELECT, DELETE
10 public.teamparticipation = SELECT
11 type=user
12
13
14=== modified file 'lib/lp/blueprints/model/specification.py'
15--- lib/lp/blueprints/model/specification.py 2012-09-18 19:41:02 +0000
16+++ lib/lp/blueprints/model/specification.py 2012-09-24 14:20:26 +0000
17@@ -5,6 +5,7 @@
18
19 __metaclass__ = type
20 __all__ = [
21+ 'get_specification_privacy_filter',
22 'HasSpecificationsMixin',
23 'recursive_blocked_query',
24 'recursive_dependent_query',
25@@ -31,6 +32,7 @@
26 And,
27 In,
28 Join,
29+ LeftJoin,
30 Or,
31 Select,
32 )
33@@ -724,14 +726,15 @@
34 notify(ObjectCreatedEvent(sub, user=subscribed_by))
35 return sub
36
37- def unsubscribe(self, person, unsubscribed_by):
38+ def unsubscribe(self, person, unsubscribed_by, ignore_permissions=False):
39 """See ISpecification."""
40 # see if a relevant subscription exists, and if so, delete it
41 if person is None:
42 person = unsubscribed_by
43 for sub in self.subscriptions:
44 if sub.person.id == person.id:
45- if not sub.canBeUnsubscribedByUser(unsubscribed_by):
46+ if (not sub.canBeUnsubscribedByUser(unsubscribed_by) and
47+ not ignore_permissions):
48 raise UserCannotUnsubscribePerson(
49 '%s does not have permission to unsubscribe %s.' % (
50 unsubscribed_by.displayname,
51@@ -1226,3 +1229,55 @@
52 def get(self, spec_id):
53 """See lp.blueprints.interfaces.specification.ISpecificationSet."""
54 return Specification.get(spec_id)
55+
56+
57+def get_specification_privacy_filter(user):
58+ """Return a Storm expression for filtering specifications by privacy.
59+
60+ :param user: A Person ID or a column reference.
61+ :return: A Storm expression to check if a peron has access grants
62+ for a specification.
63+ """
64+ # Avoid circular imports.
65+ from lp.registry.model.accesspolicy import (
66+ AccessArtifact,
67+ AccessPolicy,
68+ AccessPolicyGrantFlat,
69+ )
70+ public_specification_filter = (
71+ Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES))
72+ if user is None:
73+ return public_specification_filter
74+ return Or(
75+ public_specification_filter,
76+ Specification.id.is_in(
77+ Select(
78+ Specification.id,
79+ tables=(
80+ Specification,
81+ Join(
82+ AccessPolicy,
83+ And(
84+ Or(
85+ Specification.productID ==
86+ AccessPolicy.product_id,
87+ Specification.distributionID ==
88+ AccessPolicy.distribution_id),
89+ Specification.information_type ==
90+ AccessPolicy.type)),
91+ Join(
92+ AccessPolicyGrantFlat,
93+ AccessPolicy.id == AccessPolicyGrantFlat.policy_id),
94+ LeftJoin(
95+ AccessArtifact,
96+ AccessPolicyGrantFlat.abstract_artifact_id ==
97+ AccessArtifact.id),
98+ Join(
99+ TeamParticipation,
100+ And(
101+ TeamParticipation.team ==
102+ AccessPolicyGrantFlat.grantee_id,
103+ TeamParticipation.person == user))),
104+ where=Or(
105+ AccessPolicyGrantFlat.abstract_artifact_id == None,
106+ AccessArtifact.specification_id == Specification.id))))
107
108=== modified file 'lib/lp/blueprints/tests/test_specification.py'
109--- lib/lp/blueprints/tests/test_specification.py 2012-09-17 15:19:10 +0000
110+++ lib/lp/blueprints/tests/test_specification.py 2012-09-24 14:20:26 +0000
111@@ -6,6 +6,7 @@
112 __metaclass__ = type
113
114
115+from storm.store import Store
116 from zope.component import (
117 getUtility,
118 queryAdapter,
119@@ -31,7 +32,14 @@
120 )
121 from lp.blueprints.errors import TargetAlreadyHasSpecification
122 from lp.blueprints.interfaces.specification import ISpecificationSet
123-from lp.registry.enums import SharingPermission
124+from lp.blueprints.model.specification import (
125+ get_specification_privacy_filter,
126+ Specification,
127+ )
128+from lp.registry.enums import (
129+ SharingPermission,
130+ SpecificationSharingPolicy,
131+ )
132 from lp.security import (
133 AdminSpecification,
134 EditSpecificationByRelatedPeople,
135@@ -409,6 +417,49 @@
136 specification.target.owner, specification,
137 error_expected=False, attribute='name', value='foo')
138
139+ def test_get_specification_privacy_filter(self):
140+ # get_specification_privacy_filter() returns a Storm expression
141+ # that can be used to filter specifications by their visibility-
142+ owner = self.factory.makePerson()
143+ product = self.factory.makeProduct(
144+ owner=owner,
145+ specification_sharing_policy=(
146+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
147+ public_spec = self.factory.makeSpecification(product=product)
148+ proprietary_spec_1 = self.factory.makeSpecification(
149+ product=product, information_type=InformationType.PROPRIETARY)
150+ proprietary_spec_2 = self.factory.makeSpecification(
151+ product=product, information_type=InformationType.PROPRIETARY)
152+ all_specs = [
153+ public_spec, proprietary_spec_1, proprietary_spec_2]
154+ store = Store.of(product)
155+ specs_for_anon = store.find(
156+ Specification, get_specification_privacy_filter(None),
157+ Specification.productID == product.id)
158+ self.assertContentEqual([public_spec], specs_for_anon)
159+ # Product owners havae grants on the product, the privacy
160+ # filter returns thus all specifications for them.
161+ specs_for_owner = store.find(
162+ Specification, get_specification_privacy_filter(owner.id),
163+ Specification.productID == product.id)
164+ self.assertContentEqual(all_specs, specs_for_owner)
165+ # The filter returns only public specs for ordinary users.
166+ user = self.factory.makePerson()
167+ specs_for_other_user = store.find(
168+ Specification, get_specification_privacy_filter(user.id),
169+ Specification.productID == product.id)
170+ self.assertContentEqual([public_spec], specs_for_other_user)
171+ # If the user has a grant for a specification, the filter returns
172+ # this specification too.
173+ with person_logged_in(owner):
174+ getUtility(IService, 'sharing').ensureAccessGrants(
175+ [user], owner, specifications=[proprietary_spec_1])
176+ specs_for_other_user = store.find(
177+ Specification, get_specification_privacy_filter(user.id),
178+ Specification.productID == product.id)
179+ self.assertContentEqual(
180+ [public_spec, proprietary_spec_1], specs_for_other_user)
181+
182
183 class TestSpecificationSet(TestCaseWithFactory):
184
185
186=== modified file 'lib/lp/registry/model/sharingjob.py'
187--- lib/lp/registry/model/sharingjob.py 2012-09-18 19:41:02 +0000
188+++ lib/lp/registry/model/sharingjob.py 2012-09-24 14:20:26 +0000
189@@ -46,6 +46,14 @@
190 IBug,
191 IBugSet,
192 )
193+from lp.blueprints.interfaces.specification import ISpecification
194+from lp.blueprints.model.specification import (
195+ get_specification_privacy_filter,
196+ Specification,
197+ )
198+from lp.blueprints.model.specificationsubscription import (
199+ SpecificationSubscription,
200+ )
201 from lp.bugs.model.bugsubscription import BugSubscription
202 from lp.bugs.model.bugtaskflat import BugTaskFlat
203 from lp.bugs.model.bugtasksearch import get_bug_privacy_filter_terms
204@@ -265,18 +273,25 @@
205
206 bug_ids = []
207 branch_ids = []
208+ specification_ids = []
209 if artifacts:
210 for artifact in artifacts:
211 if IBug.providedBy(artifact):
212 bug_ids.append(artifact.id)
213 elif IBranch.providedBy(artifact):
214 branch_ids.append(artifact.id)
215+ elif ISpecification.providedBy(artifact):
216+ specification_ids.append(artifact.id)
217+ else:
218+ raise ValueError(
219+ 'Unsupported artifact: %r' % artifact)
220 information_types = [
221 info_type.value for info_type in information_types or []
222 ]
223 metadata = {
224 'bug_ids': bug_ids,
225 'branch_ids': branch_ids,
226+ 'specification_ids': specification_ids,
227 'information_types': information_types,
228 'requestor.id': requestor.id
229 }
230@@ -308,6 +323,10 @@
231 return [getUtility(IBranchLookup).get(id) for id in self.branch_ids]
232
233 @property
234+ def specification_ids(self):
235+ return self.metadata.get('specification_ids', [])
236+
237+ @property
238 def information_types(self):
239 if not 'information_types' in self.metadata:
240 return []
241@@ -332,6 +351,7 @@
242 'requestor': self.requestor.name,
243 'bug_ids': self.bug_ids,
244 'branch_ids': self.branch_ids,
245+ 'specification_ids': self.specification_ids,
246 'pillar': getattr(self.pillar, 'name', None),
247 'grantee': getattr(self.grantee, 'name', None)
248 }
249@@ -346,9 +366,13 @@
250
251 bug_filters = []
252 branch_filters = []
253+ specification_filters = []
254
255 if self.branch_ids:
256 branch_filters.append(Branch.id.is_in(self.branch_ids))
257+ if self.specification_ids:
258+ specification_filters.append(Specification.id.is_in(
259+ self.specification_ids))
260 if self.bug_ids:
261 bug_filters.append(BugTaskFlat.bug_id.is_in(self.bug_ids))
262 else:
263@@ -358,14 +382,21 @@
264 self.information_types))
265 branch_filters.append(
266 Branch.information_type.is_in(self.information_types))
267+ specification_filters.append(
268+ Specification.information_type.is_in(
269+ self.information_types))
270 if self.product:
271 bug_filters.append(
272 BugTaskFlat.product == self.product)
273 branch_filters.append(Branch.product == self.product)
274+ specification_filters.append(
275+ Specification.product == self.product)
276 if self.distro:
277 bug_filters.append(
278 BugTaskFlat.distribution == self.distro)
279 branch_filters.append(Branch.distribution == self.distro)
280+ specification_filters.append(
281+ Specification.distribution == self.distro)
282
283 if self.grantee:
284 bug_filters.append(
285@@ -378,6 +409,11 @@
286 Select(
287 TeamParticipation.personID,
288 where=TeamParticipation.team == self.grantee)))
289+ specification_filters.append(
290+ In(SpecificationSubscription.personID,
291+ Select(
292+ TeamParticipation.personID,
293+ where=TeamParticipation.team == self.grantee)))
294
295 if bug_filters:
296 bug_filters.append(Not(
297@@ -402,3 +438,20 @@
298 for sub in branch_subscriptions:
299 sub.branch.unsubscribe(
300 sub.person, self.requestor, ignore_permissions=True)
301+ if specification_filters:
302+ specification_filters.append(
303+ Not(get_specification_privacy_filter(
304+ SpecificationSubscription.personID)))
305+ tables = (
306+ SpecificationSubscription,
307+ Join(
308+ Specification,
309+ Specification.id ==
310+ SpecificationSubscription.specificationID))
311+ specifications_subscriptions = IStore(
312+ SpecificationSubscription).using(*tables).find(
313+ SpecificationSubscription, *specification_filters).config(
314+ distinct=True)
315+ for sub in specifications_subscriptions:
316+ sub.specification.unsubscribe(
317+ sub.person, self.requestor, ignore_permissions=True)
318
319=== modified file 'lib/lp/registry/tests/test_sharingjob.py'
320--- lib/lp/registry/tests/test_sharingjob.py 2012-09-18 19:41:02 +0000
321+++ lib/lp/registry/tests/test_sharingjob.py 2012-09-24 14:20:26 +0000
322@@ -17,6 +17,7 @@
323 BranchSubscriptionNotificationLevel,
324 CodeReviewNotificationLevel,
325 )
326+from lp.registry.enums import SpecificationSharingPolicy
327 from lp.registry.interfaces.accesspolicy import (
328 IAccessArtifactGrantSource,
329 IAccessPolicySource,
330@@ -117,6 +118,16 @@
331 'for branch_ids=[%d], requestor=%s>'
332 % (branch.id, requestor.name), repr(job))
333
334+ def test_repr_specifications(self):
335+ requestor = self.factory.makePerson()
336+ specification = self.factory.makeSpecification()
337+ job = getUtility(IRemoveArtifactSubscriptionsJobSource).create(
338+ requestor, artifacts=[specification])
339+ self.assertEqual(
340+ '<REMOVE_ARTIFACT_SUBSCRIPTIONS job reconciling subscriptions '
341+ 'for requestor=%s, specification_ids=[%d]>'
342+ % (requestor.name, specification.id), repr(job))
343+
344 def test_create_success(self):
345 # Create an instance of SharingJobDerived that delegates to SharingJob.
346 self.assertIs(True, ISharingJobSource.providedBy(SharingJobDerived))
347@@ -244,6 +255,17 @@
348 self.assertEqual(requestor.id, naked_job.requestor_id)
349 self.assertContentEqual([bug.id], naked_job.bug_ids)
350
351+ def test_create__bad_artifact_class(self):
352+ # A ValueError is raised if an object of an unsupported type
353+ # is passed as an artifact to
354+ # IRemoveArtifactSubscriptionsJob.create().
355+ requestor = self.factory.makePerson()
356+ wrong_object = self.factory.makePerson()
357+ self.assertRaises(
358+ ValueError,
359+ getUtility(IRemoveArtifactSubscriptionsJobSource).create,
360+ requestor, [wrong_object])
361+
362 def test_getErrorRecipients(self):
363 # The pillar owner and job requestor are the error recipients.
364 requestor = self.factory.makePerson()
365@@ -257,10 +279,15 @@
366 self.assertContentEqual(
367 expected_emails, job.getErrorRecipients())
368
369- def _assert_bug_change_unsubscribes(self, change_callback):
370- # Subscribers are unsubscribed if the bug becomes invisible due to a
371- # change in information_type.
372- product = self.factory.makeProduct()
373+ def _assert_artifact_change_unsubscribes(self, change_callback,
374+ configure_test):
375+ # Subscribers are unsubscribed if the artifact becomes invisible
376+ # due to a change in information_type.
377+ product = self.factory.makeProduct(
378+ specification_sharing_policy=(
379+ SpecificationSharingPolicy.EMBARGOED_OR_PROPRIETARY))
380+ self.factory.makeAccessPolicy(product, InformationType.PROPRIETARY)
381+ self.factory.makeAccessPolicy(product, InformationType.EMBARGOED)
382 owner = self.factory.makePerson()
383 [policy] = getUtility(IAccessPolicySource).find(
384 [(product, InformationType.USERDATA)])
385@@ -278,6 +305,9 @@
386 branch = self.factory.makeBranch(
387 owner=owner, product=product,
388 information_type=InformationType.USERDATA)
389+ specification = self.factory.makeSpecification(
390+ owner=owner, product=product,
391+ information_type=InformationType.PROPRIETARY)
392
393 # The artifact grantees will not lose access when the job is run.
394 artifact_indirect_grantee = self.factory.makePerson()
395@@ -285,113 +315,127 @@
396 membership_policy=TeamMembershipPolicy.RESTRICTED,
397 members=[artifact_indirect_grantee])
398
399- bug.subscribe(policy_team_grantee, owner)
400- bug.subscribe(policy_indirect_grantee, owner)
401- bug.subscribe(artifact_team_grantee, owner)
402 bug.subscribe(artifact_indirect_grantee, owner)
403 branch.subscribe(artifact_indirect_grantee,
404 BranchSubscriptionNotificationLevel.NOEMAIL, None,
405 CodeReviewNotificationLevel.NOEMAIL, owner)
406+ # Subscribing somebody to a specification does not automatically
407+ # create an artifact grant.
408+ spec_artifact = self.factory.makeAccessArtifact(specification)
409+ self.factory.makeAccessArtifactGrant(
410+ spec_artifact, artifact_indirect_grantee)
411+ specification.subscribe(artifact_indirect_grantee, owner)
412+
413+ # pick one of the concrete artifacts (bug, branch or spec)
414+ # and subscribe the teams and persons.
415+ concrete_artifact, get_pillars, get_subscribers = configure_test(
416+ bug, branch, specification, policy_team_grantee,
417+ policy_indirect_grantee, artifact_team_grantee, owner)
418+
419 # Subscribing policy_team_grantee has created an artifact grant so we
420 # need to revoke that to test the job.
421- artifact = self.factory.makeAccessArtifact(concrete=bug)
422+ artifact = self.factory.makeAccessArtifact(concrete=concrete_artifact)
423 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
424 [artifact], [policy_team_grantee])
425
426 # policy grantees are subscribed because the job has not been run yet.
427- subscribers = removeSecurityProxy(bug).getDirectSubscribers()
428+ subscribers = get_subscribers(concrete_artifact)
429 self.assertIn(policy_team_grantee, subscribers)
430 self.assertIn(policy_indirect_grantee, subscribers)
431
432- # Change bug attributes so that it can become inaccessible for
433+ # Change artifact attributes so that it can become inaccessible for
434 # some users.
435- change_callback(bug)
436+ change_callback(concrete_artifact)
437 reconcile_access_for_artifact(
438- bug, bug.information_type, bug.affected_pillars)
439+ concrete_artifact, concrete_artifact.information_type,
440+ get_pillars(concrete_artifact))
441
442 getUtility(IRemoveArtifactSubscriptionsJobSource).create(
443- owner, [bug])
444+ owner, [concrete_artifact])
445 with block_on_job(self):
446 transaction.commit()
447
448 # Check the result. Policy grantees will be unsubscribed.
449- subscribers = removeSecurityProxy(bug).getDirectSubscribers()
450+ subscribers = get_subscribers(concrete_artifact)
451 self.assertNotIn(policy_team_grantee, subscribers)
452 self.assertNotIn(policy_indirect_grantee, subscribers)
453 self.assertIn(artifact_team_grantee, subscribers)
454- self.assertIn(artifact_indirect_grantee, subscribers)
455+ self.assertIn(artifact_indirect_grantee, bug.getDirectSubscribers())
456 self.assertIn(artifact_indirect_grantee, branch.subscribers)
457+ self.assertIn(artifact_indirect_grantee, specification.subscribers)
458+
459+ def _assert_bug_change_unsubscribes(self, change_callback):
460+
461+ def get_pillars(concrete_artifact):
462+ return concrete_artifact.affected_pillars
463+
464+ def get_subscribers(concrete_artifact):
465+ return removeSecurityProxy(
466+ concrete_artifact).getDirectSubscribers()
467+
468+ def configure_test(bug, branch, specification, policy_team_grantee,
469+ policy_indirect_grantee, artifact_team_grantee,
470+ owner):
471+ concrete_artifact = bug
472+ bug.subscribe(policy_team_grantee, owner)
473+ bug.subscribe(policy_indirect_grantee, owner)
474+ bug.subscribe(artifact_team_grantee, owner)
475+ return concrete_artifact, get_pillars, get_subscribers
476+
477+ self._assert_artifact_change_unsubscribes(
478+ change_callback, configure_test)
479
480 def _assert_branch_change_unsubscribes(self, change_callback):
481- product = self.factory.makeProduct()
482- owner = self.factory.makePerson()
483- [policy] = getUtility(IAccessPolicySource).find(
484- [(product, InformationType.USERDATA)])
485- # The policy grantees will lose access.
486- policy_indirect_grantee = self.factory.makePerson()
487- policy_team_grantee = self.factory.makeTeam(
488- membership_policy=TeamMembershipPolicy.RESTRICTED,
489- members=[policy_indirect_grantee])
490-
491- self.factory.makeAccessPolicyGrant(policy, policy_team_grantee, owner)
492- login_person(owner)
493- bug = self.factory.makeBug(
494- owner=owner, target=product,
495- information_type=InformationType.USERDATA)
496- branch = self.factory.makeBranch(
497- owner=owner, product=product,
498- information_type=InformationType.USERDATA)
499-
500- # The artifact grantees will not lose access when the job is run.
501- artifact_indirect_grantee = self.factory.makePerson()
502- artifact_team_grantee = self.factory.makeTeam(
503- membership_policy=TeamMembershipPolicy.RESTRICTED,
504- members=[artifact_indirect_grantee])
505-
506- branch.subscribe(
507- policy_team_grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
508- None, CodeReviewNotificationLevel.NOEMAIL, owner)
509- branch.subscribe(
510- policy_indirect_grantee,
511- BranchSubscriptionNotificationLevel.NOEMAIL, None,
512- CodeReviewNotificationLevel.NOEMAIL, owner)
513- branch.subscribe(
514- artifact_team_grantee,
515- BranchSubscriptionNotificationLevel.NOEMAIL, None,
516- CodeReviewNotificationLevel.NOEMAIL, owner)
517- branch.subscribe(
518- artifact_indirect_grantee,
519- BranchSubscriptionNotificationLevel.NOEMAIL, None,
520- CodeReviewNotificationLevel.NOEMAIL, owner)
521- bug.subscribe(artifact_indirect_grantee, owner)
522- # Subscribing policy_team_grantee has created an artifact grant so we
523- # need to revoke that to test the job.
524- artifact = self.factory.makeAccessArtifact(concrete=branch)
525- getUtility(IAccessArtifactGrantSource).revokeByArtifact(
526- [artifact], [policy_team_grantee])
527-
528- # policy grantees are subscribed because the job has not been run yet.
529- #subscribers = removeSecurityProxy(branch).subscribers
530- self.assertIn(policy_team_grantee, branch.subscribers)
531- self.assertIn(policy_indirect_grantee, branch.subscribers)
532-
533- # Change branch attributes so that it can become inaccessible for
534- # some users.
535- change_callback(branch)
536- reconcile_access_for_artifact(
537- branch, branch.information_type, [branch.product])
538-
539- getUtility(IRemoveArtifactSubscriptionsJobSource).create(
540- owner, [branch])
541- with block_on_job(self):
542- transaction.commit()
543-
544- # Check the result. Policy grantees will be unsubscribed.
545- self.assertNotIn(policy_team_grantee, branch.subscribers)
546- self.assertNotIn(policy_indirect_grantee, branch.subscribers)
547- self.assertIn(artifact_team_grantee, branch.subscribers)
548- self.assertIn(artifact_indirect_grantee, branch.subscribers)
549- self.assertIn(artifact_indirect_grantee, bug.getDirectSubscribers())
550+
551+ def get_pillars(concrete_artifact):
552+ return [concrete_artifact.product]
553+
554+ def get_subscribers(concrete_artifact):
555+ return concrete_artifact.subscribers
556+
557+ def configure_test(bug, branch, specification, policy_team_grantee,
558+ policy_indirect_grantee, artifact_team_grantee,
559+ owner):
560+ concrete_artifact = branch
561+ branch.subscribe(
562+ policy_team_grantee,
563+ BranchSubscriptionNotificationLevel.NOEMAIL,
564+ None, CodeReviewNotificationLevel.NOEMAIL, owner)
565+ branch.subscribe(
566+ policy_indirect_grantee,
567+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
568+ CodeReviewNotificationLevel.NOEMAIL, owner)
569+ branch.subscribe(
570+ artifact_team_grantee,
571+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
572+ CodeReviewNotificationLevel.NOEMAIL, owner)
573+ return concrete_artifact, get_pillars, get_subscribers
574+
575+ self._assert_artifact_change_unsubscribes(
576+ change_callback, configure_test)
577+
578+ def _assert_specification_change_unsubscribes(self, change_callback):
579+
580+ def get_pillars(concrete_artifact):
581+ return [concrete_artifact.product]
582+
583+ def get_subscribers(concrete_artifact):
584+ return concrete_artifact.subscribers
585+
586+ def configure_test(bug, branch, specification, policy_team_grantee,
587+ policy_indirect_grantee, artifact_team_grantee,
588+ owner):
589+ concrete_artifact = specification
590+ specification.subscribe(policy_team_grantee, owner)
591+ specification.subscribe(policy_indirect_grantee, owner)
592+ spec_artifact = self.factory.makeAccessArtifact(specification)
593+ self.factory.makeAccessArtifactGrant(
594+ spec_artifact, artifact_team_grantee)
595+ specification.subscribe(artifact_team_grantee, owner)
596+ return concrete_artifact, get_pillars, get_subscribers
597+
598+ self._assert_artifact_change_unsubscribes(
599+ change_callback, configure_test)
600
601 def test_change_information_type_branch(self):
602 def change_information_type(branch):
603@@ -400,6 +444,13 @@
604
605 self._assert_branch_change_unsubscribes(change_information_type)
606
607+ def test_change_information_type_specification(self):
608+ def change_information_type(specification):
609+ removeSecurityProxy(specification).information_type = (
610+ InformationType.EMBARGOED)
611+
612+ self._assert_specification_change_unsubscribes(change_information_type)
613+
614 def test_change_information_type(self):
615 # Changing the information type of a bug unsubscribes users who can no
616 # longer see the bug.