Merge lp:~abentley/launchpad/private-product-listings into lp:launchpad

Proposed by Aaron Bentley on 2012-10-09
Status: Merged
Approved by: Aaron Bentley on 2012-10-16
Approved revision: no longer in the source branch.
Merged at revision: 16153
Proposed branch: lp:~abentley/launchpad/private-product-listings
Merge into: lp:launchpad
Diff against target: 523 lines (+268/-21)
8 files modified
lib/lp/registry/browser/product.py (+7/-3)
lib/lp/registry/browser/tests/test_product.py (+79/-0)
lib/lp/registry/doc/commercialsubscription.txt (+21/-8)
lib/lp/registry/interfaces/product.py (+3/-1)
lib/lp/registry/model/product.py (+30/-7)
lib/lp/registry/scripts/productreleasefinder/finder.py (+1/-1)
lib/lp/registry/templates/products-index.pt (+1/-1)
lib/lp/registry/tests/test_product.py (+126/-0)
To merge this branch: bzr merge lp:~abentley/launchpad/private-product-listings
Reviewer Review Type Date Requested Status
Richard Harding (community) 2012-10-09 Approve on 2012-10-16
Review via email: mp+128800@code.launchpad.net

Commit Message

Do not attempt to list Products for users who cannot view them.

Description of the Change

= Summary =
Fix bug #1063264: Cannot view /projects when a private project is in the list

== Proposed fix ==
List only those products that the user can view.

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of Private Projects

== Implementation details ==
Launchpad admins and commercial admins should be able to review all products. This is handled in getProductPrivacyFilter, but Product.userCanView did not implement this, so it was added.

ProductSetView.latest was implemented so that it could supply self.user to get_all_active.

== Tests ==
bin/test -t getProductPrivacyFilter -t test_get_all_active_omits_proprietary -t
test_admin_launchpad_View_proprietary_product -t test_review_include_proprietary_for_admin -t test_review_exclude_proprietary_for_expert -t test_proprietary_products_shown_to_owners_all -t test_proprietary_products_skipped_all -t test_proprietary_products_shown_to_owners -t test_proprietary_products_skipped

== Demo and Q/A ==
Create a proprietary product. It should be visible in /projects and projects/+all. Log in as a different user. It should not be visible. Share the product with another user. It should be visible to that user.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/doc/commercialsubscription.txt
  lib/lp/registry/browser/product.py
  lib/lp/registry/browser/tests/test_product.py
  lib/lp/registry/model/product.py
  lib/lp/registry/templates/products-index.pt
  lib/lp/registry/tests/test_product.py

To post a comment you must log in.
Richard Harding (rharding) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/product.py'
2--- lib/lp/registry/browser/product.py 2012-10-12 19:34:12 +0000
3+++ lib/lp/registry/browser/product.py 2012-10-16 14:34:27 +0000
4@@ -1700,7 +1700,8 @@
5
6 @cachedproperty
7 def all_batched(self):
8- return BatchNavigator(self.context.all_active, self.request)
9+ return BatchNavigator(self.context.get_all_active(self.user),
10+ self.request)
11
12 @cachedproperty
13 def matches(self):
14@@ -1718,6 +1719,9 @@
15 def tooManyResultsFound(self):
16 return self.matches > self.max_results_to_display
17
18+ def latest(self):
19+ return self.context.get_all_active(self.user)[:5]
20+
21
22 class ProductSetReviewLicensesView(LaunchpadFormView):
23 """View for searching products to be reviewed."""
24@@ -1793,8 +1797,8 @@
25 search_params = self.initial_values
26 # Override the defaults with the form values if available.
27 search_params.update(data)
28- return BatchNavigator(self.context.forReview(**search_params),
29- self.request, size=50)
30+ result = self.context.forReview(self.user, **search_params)
31+ return BatchNavigator(result, self.request, size=50)
32
33
34 class ProductAddViewBase(ProductLicenseMixin, LaunchpadFormView):
35
36=== modified file 'lib/lp/registry/browser/tests/test_product.py'
37--- lib/lp/registry/browser/tests/test_product.py 2012-10-15 09:15:29 +0000
38+++ lib/lp/registry/browser/tests/test_product.py 2012-10-16 14:34:27 +0000
39@@ -637,3 +637,82 @@
40 content_disposition, browser.headers['Content-disposition'])
41 self.assertEqual(
42 'application/rdf+xml', browser.headers['Content-type'])
43+
44+
45+class TestProductSet(BrowserTestCase):
46+
47+ layer = DatabaseFunctionalLayer
48+
49+ def makeAllInformationTypes(self):
50+ owner = self.factory.makePerson()
51+ public = self.factory.makeProduct(
52+ information_type=InformationType.PUBLIC, owner=owner)
53+ proprietary = self.factory.makeProduct(
54+ information_type=InformationType.PROPRIETARY, owner=owner)
55+ embargoed = self.factory.makeProduct(
56+ information_type=InformationType.EMBARGOED, owner=owner)
57+ return owner, public, proprietary, embargoed
58+
59+ def test_proprietary_products_skipped(self):
60+ # Ignore proprietary products for anonymous users
61+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
62+ browser = self.getViewBrowser(getUtility(IProductSet))
63+ with person_logged_in(owner):
64+ self.assertIn(public.name, browser.contents)
65+ self.assertNotIn(proprietary.name, browser.contents)
66+ self.assertNotIn(embargoed.name, browser.contents)
67+
68+ def test_proprietary_products_shown_to_owners(self):
69+ # Owners will see their proprietary products listed
70+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
71+ transaction.commit()
72+ browser = self.getViewBrowser(getUtility(IProductSet), user=owner)
73+ with person_logged_in(owner):
74+ self.assertIn(public.name, browser.contents)
75+ self.assertIn(proprietary.name, browser.contents)
76+ self.assertIn(embargoed.name, browser.contents)
77+
78+ def test_proprietary_products_skipped_all(self):
79+ # Ignore proprietary products for anonymous users
80+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
81+ product_set = getUtility(IProductSet)
82+ browser = self.getViewBrowser(product_set, view_name='+all')
83+ with person_logged_in(owner):
84+ self.assertIn(public.name, browser.contents)
85+ self.assertNotIn(proprietary.name, browser.contents)
86+ self.assertNotIn(embargoed.name, browser.contents)
87+
88+ def test_proprietary_products_shown_to_owners_all(self):
89+ # Owners will see their proprietary products listed
90+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
91+ transaction.commit()
92+ browser = self.getViewBrowser(getUtility(IProductSet), user=owner,
93+ view_name='+all')
94+ with person_logged_in(owner):
95+ self.assertIn(public.name, browser.contents)
96+ self.assertIn(proprietary.name, browser.contents)
97+ self.assertIn(embargoed.name, browser.contents)
98+
99+ def test_review_exclude_proprietary_for_expert(self):
100+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
101+ transaction.commit()
102+ expert = self.factory.makeRegistryExpert()
103+ browser = self.getViewBrowser(getUtility(IProductSet),
104+ view_name='+review-licenses',
105+ user=expert)
106+ with person_logged_in(owner):
107+ self.assertIn(public.name, browser.contents)
108+ self.assertNotIn(proprietary.name, browser.contents)
109+ self.assertNotIn(embargoed.name, browser.contents)
110+
111+ def test_review_include_proprietary_for_admin(self):
112+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
113+ transaction.commit()
114+ admin = self.factory.makeAdministrator()
115+ browser = self.getViewBrowser(getUtility(IProductSet),
116+ view_name='+review-licenses',
117+ user=admin)
118+ with person_logged_in(owner):
119+ self.assertIn(public.name, browser.contents)
120+ self.assertIn(proprietary.name, browser.contents)
121+ self.assertIn(embargoed.name, browser.contents)
122
123=== modified file 'lib/lp/registry/doc/commercialsubscription.txt'
124--- lib/lp/registry/doc/commercialsubscription.txt 2012-07-07 13:01:46 +0000
125+++ lib/lp/registry/doc/commercialsubscription.txt 2012-10-16 14:34:27 +0000
126@@ -351,7 +351,8 @@
127 >>> from datetime import timedelta
128 >>> bzr.licenses = [License.GNU_GPL_V2, License.ECLIPSE]
129 >>> flush_database_updates()
130- >>> for product in product_set.forReview(search_text='gnome'):
131+ >>> for product in product_set.forReview(commercial_member,
132+ ... search_text='gnome'):
133 ... print product.displayname
134 python gnome2 dev
135 Evolution
136@@ -362,7 +363,8 @@
137 The license_info field is also searched for matching search_text:
138
139 >>> bzr.license_info = 'Code in /contrib is under a mit-like licence.'
140- >>> for product in product_set.forReview(search_text='mit'):
141+ >>> for product in product_set.forReview(commercial_member,
142+ ... search_text='mit'):
143 ... print product.name
144 bzr
145
146@@ -372,20 +374,22 @@
147 >>> with celebrity_logged_in('registry_experts'):
148 ... bzr.reviewer_whiteboard = (
149 ... 'cc-nc discriminates against commercial uses.')
150- >>> for product in product_set.forReview(search_text='cc-nc'):
151+ >>> for product in product_set.forReview(commercial_member,
152+ ... search_text='cc-nc'):
153 ... print product.name
154 bzr
155
156 You can search for whether the product is active or not.
157
158- >>> for product in product_set.forReview(active=False):
159+ >>> for product in product_set.forReview(commercial_member, active=False):
160 ... print product.name
161 python-gnome2-dev
162 unassigned
163
164 You can search for whether the product is marked reviewed or not.
165
166- >>> for product in product_set.forReview(project_reviewed=True):
167+ >>> for product in product_set.forReview(commercial_member,
168+ ... project_reviewed=True):
169 ... print product.name
170 python-gnome2-dev
171 unassigned
172@@ -396,6 +400,7 @@
173 any one of the licences listed.
174
175 >>> for product in product_set.forReview(
176+ ... commercial_member,
177 ... licenses=[License.GNU_GPL_V2, License.BSD]):
178 ... print product.name
179 bzr
180@@ -404,6 +409,7 @@
181 not approved
182
183 >>> for product in product_set.forReview(
184+ ... commercial_member,
185 ... project_reviewed=True, license_approved=False):
186 ... print product.name
187 python-gnome2-dev
188@@ -414,6 +420,7 @@
189 was created.
190
191 >>> for product in product_set.forReview(
192+ ... commercial_member,
193 ... search_text='bzr',
194 ... created_after=bzr.datecreated,
195 ... created_before=bzr.datecreated):
196@@ -425,6 +432,7 @@
197
198 >>> date_expires = bzr.commercial_subscription.date_expires
199 >>> for product in product_set.forReview(
200+ ... commercial_member,
201 ... search_text='bzr',
202 ... subscription_expires_after=date_expires,
203 ... subscription_expires_before=date_expires):
204@@ -439,6 +447,7 @@
205 >>> early_date = date(1980, 1, 1)
206 >>> late_date = date_expires + timedelta(days=365 * 100)
207 >>> for product in product_set.forReview(
208+ ... commercial_member,
209 ... search_text='bzr',
210 ... subscription_expires_after=date_expires,
211 ... subscription_expires_before=date_expires + one_day,
212@@ -452,6 +461,7 @@
213 A reviewer can search for projects without a commercial subscription.
214
215 >>> for product in product_set.forReview(
216+ ... commercial_member,
217 ... has_subscription=False, licenses=[License.OTHER_PROPRIETARY]):
218 ... print product.name
219 mega-money-maker
220@@ -462,6 +472,7 @@
221
222 >>> date_last_modified = bzr.commercial_subscription.date_last_modified
223 >>> for product in product_set.forReview(
224+ ... commercial_member,
225 ... search_text='bzr',
226 ... subscription_modified_after=date_last_modified,
227 ... subscription_modified_before=date_last_modified):
228@@ -471,14 +482,16 @@
229 All the products are returned when no parameters are passed in.
230
231 >>> from lp.registry.model.product import Product
232- >>> product_set.forReview().count() == Product.select().count()
233+ >>> review_listing = product_set.forReview(commercial_member)
234+ >>> review_listing.count() == Product.select().count()
235 True
236
237 The full text search will not match strings with dots in their name
238 but a clause is included to search specifically for the name.
239
240 >>> new_product = factory.makeProduct(name="abc.com")
241- >>> for product in product_set.forReview(search_text="abc.com"):
242+ >>> for product in product_set.forReview(commercial_member,
243+ ... search_text="abc.com"):
244 ... print product.name
245 abc.com
246
247@@ -488,7 +501,7 @@
248 >>> login('no-priv@canonical.com')
249 >>> check_permission('launchpad.Moderate', product_set)
250 False
251- >>> gnome = product_set.forReview(search_text='gnome')
252+ >>> gnome = product_set.forReview(commercial_member, search_text='gnome')
253 Traceback (most recent call last):
254 ...
255 Unauthorized:... 'forReview', 'launchpad.Moderate'...
256
257=== modified file 'lib/lp/registry/interfaces/product.py'
258--- lib/lp/registry/interfaces/product.py 2012-10-12 14:53:10 +0000
259+++ lib/lp/registry/interfaces/product.py 2012-10-16 14:34:27 +0000
260@@ -1026,7 +1026,9 @@
261 @operation_returns_collection_of(IProduct)
262 @export_read_operation()
263 @export_operation_as('licensing_search')
264- def forReview(search_text=None,
265+ @call_with(user=REQUEST_USER)
266+ def forReview(user,
267+ search_text=None,
268 active=None,
269 project_reviewed=None,
270 licenses=None,
271
272=== modified file 'lib/lp/registry/model/product.py'
273--- lib/lp/registry/model/product.py 2012-10-15 09:15:29 +0000
274+++ lib/lp/registry/model/product.py 2012-10-16 14:34:27 +0000
275@@ -54,6 +54,10 @@
276 )
277 from zope.security.proxy import removeSecurityProxy
278
279+from lp.registry.model.accesspolicy import (
280+ AccessPolicy,
281+ AccessPolicyGrantFlat,
282+ )
283 from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH
284 from lp.answers.interfaces.faqtarget import IFAQTarget
285 from lp.answers.model.faq import (
286@@ -174,6 +178,7 @@
287 from lp.registry.model.productlicense import ProductLicense
288 from lp.registry.model.productrelease import ProductRelease
289 from lp.registry.model.productseries import ProductSeries
290+from lp.registry.model.teammembership import TeamParticipation
291 from lp.registry.model.series import ACTIVE_STATUSES
292 from lp.registry.model.sourcepackagename import SourcePackageName
293 from lp.services.database import bulk
294@@ -1729,11 +1734,29 @@
295
296 @property
297 def all_active(self):
298- return self.get_all_active()
299-
300- def get_all_active(self, eager_load=True):
301- result = IStore(Product).find(Product, Product.active
302- ).order_by(Desc(Product.datecreated))
303+ return self.get_all_active(None)
304+
305+ @staticmethod
306+ def getProductPrivacyFilter(user):
307+ if user is not None:
308+ roles = IPersonRoles(user)
309+ if roles.in_admin or roles.in_commercial_admin:
310+ return True
311+ granted_products = And(
312+ AccessPolicyGrantFlat.grantee_id == TeamParticipation.teamID,
313+ TeamParticipation.person == user,
314+ AccessPolicyGrantFlat.policy == AccessPolicy.id,
315+ AccessPolicy.product == Product.id,
316+ AccessPolicy.type == Product._information_type)
317+ return Or(Product._information_type == InformationType.PUBLIC,
318+ Product._information_type == None,
319+ Product.id.is_in(Select(Product.id, granted_products)))
320+
321+ @classmethod
322+ def get_all_active(cls, user, eager_load=True):
323+ clause = cls.getProductPrivacyFilter(user)
324+ result = IStore(Product).find(Product, Product.active,
325+ clause).order_by(Desc(Product.datecreated))
326 if not eager_load:
327 return result
328
329@@ -1843,7 +1866,7 @@
330 product.development_focus = trunk
331 return product
332
333- def forReview(self, search_text=None, active=None,
334+ def forReview(self, user, search_text=None, active=None,
335 project_reviewed=None, license_approved=None, licenses=None,
336 created_after=None, created_before=None,
337 has_subscription=None,
338@@ -1853,7 +1876,7 @@
339 subscription_modified_before=None):
340 """See lp.registry.interfaces.product.IProductSet."""
341
342- conditions = []
343+ conditions = [self.getProductPrivacyFilter(user)]
344
345 if project_reviewed is not None:
346 conditions.append(Product.project_reviewed == project_reviewed)
347
348=== modified file 'lib/lp/registry/scripts/productreleasefinder/finder.py'
349--- lib/lp/registry/scripts/productreleasefinder/finder.py 2012-09-04 20:50:35 +0000
350+++ lib/lp/registry/scripts/productreleasefinder/finder.py 2012-10-16 14:34:27 +0000
351@@ -103,7 +103,7 @@
352
353 self.ztm.begin()
354 products = getUtility(IProductSet)
355- for product in products.get_all_active(eager_load=False):
356+ for product in products.get_all_active(None, eager_load=False):
357 filters = []
358
359 for series in product.series:
360
361=== modified file 'lib/lp/registry/templates/products-index.pt'
362--- lib/lp/registry/templates/products-index.pt 2012-06-02 12:25:39 +0000
363+++ lib/lp/registry/templates/products-index.pt 2012-10-16 14:34:27 +0000
364@@ -132,7 +132,7 @@
365 <div class="portlet">
366 <h2>Latest projects registered</h2>
367 <table>
368- <tr tal:repeat="product context/latest">
369+ <tr tal:repeat="product view/latest">
370 <td>
371 <tal:link replace="structure product/fmt:link" />
372 registered
373
374=== modified file 'lib/lp/registry/tests/test_product.py'
375--- lib/lp/registry/tests/test_product.py 2012-10-15 09:15:29 +0000
376+++ lib/lp/registry/tests/test_product.py 2012-10-16 14:34:27 +0000
377@@ -71,9 +71,11 @@
378 from lp.registry.interfaces.series import SeriesStatus
379 from lp.registry.model.product import (
380 Product,
381+ ProductSet,
382 UnDeactivateable,
383 )
384 from lp.registry.model.productlicense import ProductLicense
385+from lp.services.database.lpstorm import IStore
386 from lp.services.webapp.authorization import check_permission
387 from lp.testing import (
388 celebrity_logged_in,
389@@ -757,6 +759,18 @@
390 for attribute_name in names:
391 getattr(product, attribute_name)
392
393+ def test_admin_launchpad_View_proprietary_product(self):
394+ # Admins and commercial admins can view proprietary products.
395+ product = self.factory.makeProduct(
396+ information_type=InformationType.PROPRIETARY)
397+ names = self.expected_get_permissions['launchpad.View']
398+ with person_logged_in(self.factory.makeAdministrator()):
399+ for attribute_name in names:
400+ getattr(product, attribute_name)
401+ with person_logged_in(self.factory.makeCommercialAdmin()):
402+ for attribute_name in names:
403+ getattr(product, attribute_name)
404+
405 def test_access_launchpad_AnyAllowedPerson_public_product(self):
406 # Only logged in persons have access to properties of public products
407 # that require the permission launchpad.AnyAllowedPerson.
408@@ -1624,3 +1638,115 @@
409 self.failUnlessEqual(
410 [],
411 ws_product.findReferencedOOPS(start_date=now - day, end_date=now))
412+
413+
414+class TestProductSet(TestCaseWithFactory):
415+
416+ layer = DatabaseFunctionalLayer
417+
418+ def makeAllInformationTypes(self):
419+ proprietary = self.factory.makeProduct(
420+ information_type=InformationType.PROPRIETARY)
421+ embargoed = self.factory.makeProduct(
422+ information_type=InformationType.EMBARGOED)
423+ public = self.factory.makeProduct(
424+ information_type=InformationType.PUBLIC)
425+ return proprietary, embargoed, public
426+
427+ @staticmethod
428+ def filterFind(user):
429+ clause = ProductSet.getProductPrivacyFilter(user)
430+ return IStore(Product).find(Product, clause)
431+
432+ def test_get_all_active_omits_proprietary(self):
433+ # Ignore proprietary products for anonymous users
434+ proprietary = self.factory.makeProduct(
435+ information_type=InformationType.PROPRIETARY)
436+ embargoed = self.factory.makeProduct(
437+ information_type=InformationType.EMBARGOED)
438+ result = ProductSet.get_all_active(None)
439+ self.assertNotIn(proprietary, result)
440+ self.assertNotIn(embargoed, result)
441+
442+ def test_getProductPrivacyFilterAnonymous(self):
443+ # Ignore proprietary products for anonymous users
444+ proprietary, embargoed, public = self.makeAllInformationTypes()
445+ result = self.filterFind(None)
446+ self.assertIn(public, result)
447+ self.assertNotIn(embargoed, result)
448+ self.assertNotIn(proprietary, result)
449+
450+ def test_getProductPrivacyFilter_excludes_random_users(self):
451+ # Exclude proprietary products for anonymous users
452+ random = self.factory.makePerson()
453+ proprietary, embargoed, public = self.makeAllInformationTypes()
454+ result = self.filterFind(random)
455+ self.assertIn(public, result)
456+ self.assertNotIn(embargoed, result)
457+ self.assertNotIn(proprietary, result)
458+
459+ def grant(self, pillar, information_type, grantee):
460+ policy_source = getUtility(IAccessPolicySource)
461+ (policy,) = policy_source.find(
462+ [(pillar, information_type)])
463+ self.factory.makeAccessPolicyGrant(policy, grantee)
464+
465+ def test_getProductPrivacyFilter_respects_grants(self):
466+ # Include proprietary products for users with right grants.
467+ grantee = self.factory.makePerson()
468+ proprietary, embargoed, public = self.makeAllInformationTypes()
469+ self.grant(embargoed, InformationType.EMBARGOED, grantee)
470+ self.grant(proprietary, InformationType.PROPRIETARY, grantee)
471+ result = self.filterFind(grantee)
472+ self.assertIn(public, result)
473+ self.assertIn(embargoed, result)
474+ self.assertIn(proprietary, result)
475+
476+ def test_getProductPrivacyFilter_ignores_wrong_product(self):
477+ # Exclude proprietary products if grant is on wrong product.
478+ grantee = self.factory.makePerson()
479+ proprietary, embargoed, public = self.makeAllInformationTypes()
480+ self.factory.makeAccessPolicyGrant(grantee=grantee)
481+ result = self.filterFind(grantee)
482+ self.assertIn(public, result)
483+ self.assertNotIn(embargoed, result)
484+ self.assertNotIn(proprietary, result)
485+
486+ def test_getProductPrivacyFilter_ignores_wrong_info_type(self):
487+ # Exclude proprietary products if grant is on wrong information type.
488+ grantee = self.factory.makePerson()
489+ proprietary, embargoed, public = self.makeAllInformationTypes()
490+ self.grant(embargoed, InformationType.PROPRIETARY, grantee)
491+ self.factory.makeAccessPolicy(proprietary, InformationType.EMBARGOED)
492+ self.grant(proprietary, InformationType.EMBARGOED, grantee)
493+ result = self.filterFind(grantee)
494+ self.assertIn(public, result)
495+ self.assertNotIn(embargoed, result)
496+ self.assertNotIn(proprietary, result)
497+
498+ def test_getProductPrivacyFilter_respects_team_grants(self):
499+ # Include proprietary products for users in teams with right grants.
500+ grantee = self.factory.makeTeam()
501+ proprietary, embargoed, public = self.makeAllInformationTypes()
502+ self.grant(embargoed, InformationType.EMBARGOED, grantee)
503+ self.grant(proprietary, InformationType.PROPRIETARY, grantee)
504+ result = self.filterFind(grantee.teamowner)
505+ self.assertIn(public, result)
506+ self.assertIn(embargoed, result)
507+ self.assertIn(proprietary, result)
508+
509+ def test_getProductPrivacyFilter_includes_admins(self):
510+ # Launchpad admins can see everything.
511+ proprietary, embargoed, public = self.makeAllInformationTypes()
512+ result = self.filterFind(self.factory.makeAdministrator())
513+ self.assertIn(public, result)
514+ self.assertIn(embargoed, result)
515+ self.assertIn(proprietary, result)
516+
517+ def test_getProductPrivacyFilter_includes_commercial_admins(self):
518+ # Commercial admins can see everything.
519+ proprietary, embargoed, public = self.makeAllInformationTypes()
520+ result = self.filterFind(self.factory.makeCommercialAdmin())
521+ self.assertIn(public, result)
522+ self.assertIn(embargoed, result)
523+ self.assertIn(proprietary, result)