← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/limited-view-pages-0 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/limited-view-pages-0 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/limited-view-pages-0/+merge/86112

Users cannot see pages when they have limitedview.

    Launchpad bug: https://bugs.launchpad.net/bugs/904283
    Pre-implementation: jcsackett

When a user has limitedview privileges for a private team (or project in
the future), Lp allows the user to know the identifying information
about the team. Launchpad also makes links to the team. If the user
hacks the url or follows a link, the user gets a 404 error, which is
ridiculous because the user does not the team exists and does have
permission to know the identifying information.

--------------------------------------------------------------------

RULES

    * Update the base-layout template to not render the content slots if
      the user does not have Lp.View
      * The header which contains the identifying information must
        be shown.
      * the footer can be shown
      * Notifications in the maincontent must also be shown.
      * The user must also see the application links and breadcrumbs that
        are in the maincontent div...
      * ...but not the registrant slot or the main slot.
      * The side-portlets slot cannot be rendered
      * The optional head_epilogue slot cannot be included
      * ADDENDUM, the meta description cannot be rendered.
    * In place of the main slot, the user should see an explanation
      about why other information is not available.

QA

    * Launchpad team members can use ~canonical-isd's P3A as the test.
    * As a user subscribed to a private team's archive.
    * Verify you can see the archive page.
    * Verify that when you visit the team page that it displays
      with a header, footer, and an explanation for the missing
      page content.
    * Verify you cannot see who registered the team
    * Verify each of the applicate links work, but they show the same
      presentation as the index page.


LINT

    lib/lp/app/browser/tales.py
    lib/lp/app/browser/tests/base-layout.txt
    lib/lp/app/browser/tests/test_base_layout.py
    lib/lp/app/browser/tests/test_page_macro.py
    lib/lp/app/browser/tests/testfiles/main-side.pt
    lib/lp/app/templates/base-layout.pt
    lib/lp/registry/browser/tests/test_team.py
    lib/lp/testing/__init__.py



TEST

    ./bin/test -vvc -t 'base|macro' lp.app.browser.tests
    ./bin/test -vvc lp.registry.browser.tests.test_team


IMPLEMENTATION

This Branch can be divided into 6 sets of dependent changes. Most of the
branch is dedicated to replacing doctests with extensible unittests.

Added FakeAdapterMixin that makes it easy to register adapters like views
and security checkers. This simplifies tests that want to work with
hypothetical objects or instrument rare conditions.
    lib/lp/testing/__init__.py

Replaced a doctest with a unittest that I can later extend.
    lib/lp/app/doc/tales-macro.txt
    lib/lp/app/browser/tests/test_page_macro.py

Moved the registration slot test into the unittest; fixed main-side.pt
then deleted unused main-registering.pt
    lib/lp/app/browser/tests/base-layout.txt
    lib/lp/app/browser/tests/test_base_layout.py
    lib/lp/app/browser/tests/testfiles/main-registering.pt
    lib/lp/app/browser/tests/testfiles/main-side.pt

Added a new macro (macro:is-page-contentless) that can be used by templates
to know when they should not render slots that contain content.
I discovered The show_actions_menu() was neither tests or implemented.
    lib/lp/app/browser/tales.py
    lib/lp/app/browser/tests/test_page_macro.py

Updated base layout to use macro:is-page-contentless and added a test that
reproduces the specific condition. I abandoned a test with hypothetical
objects because base-layout verifies a lot of behaviours from many
interfaces. Building a fake person and team object was unproductive.
    lib/lp/app/templates/base-layout.pt
    lib/lp/app/browser/tests/test_base_layout.py

Added an integration-like test to verify the specific condition we expect
users to view the private team index page works. This test revealed that
base-layout was creating a meta name="description" tag which we do not
want to do in the case where the user does not have Lp.View.
    lib/lp/registry/browser/tests/test_team.py
-- 
https://code.launchpad.net/~sinzui/launchpad/limited-view-pages-0/+merge/86112
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/limited-view-pages-0 into lp:launchpad.
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py	2011-12-14 08:23:39 +0000
+++ lib/lp/app/browser/tales.py	2011-12-16 20:39:26 +0000
@@ -2499,9 +2499,11 @@
         view/macro:pagehas/applicationtabs
         view/macro:pagehas/globalsearch
         view/macro:pagehas/portlets
+        view/macro:pagehas/main
 
         view/macro:pagetype
 
+        view/macro:is-page-contentless
     """
 
     implements(ITraversable)
@@ -2534,8 +2536,8 @@
             return self.haspage(layoutelement)
         elif name == 'pagetype':
             return self.pagetype()
-        elif name == 'show_actions_menu':
-            return self.show_actions_menu()
+        elif name == 'is-page-contentless':
+            return self.isPageContentless()
         else:
             raise TraversalError(name)
 
@@ -2551,6 +2553,20 @@
             pagetype = 'unset'
         return self._pagetypes[pagetype][layoutelement]
 
+    def isPageContentless(self):
+        """Should the template avoid rendering detailed information.
+
+        Circumstances such as not possessing launchpad.View on a private
+        context require the template to not render detailed information. The
+        user may only know identifying information about the context.
+        """
+        view_context = self.context.context
+        privacy = IPrivacy(view_context, None)
+        if privacy is None or not privacy.private:
+            return False
+        can_view = check_permission('launchpad.View', view_context)
+        return not can_view
+
     def pagetype(self):
         return getattr(self.context, '__pagetype__', 'unset')
 

=== modified file 'lib/lp/app/browser/tests/base-layout.txt'
--- lib/lp/app/browser/tests/base-layout.txt	2011-12-14 08:13:46 +0000
+++ lib/lp/app/browser/tests/base-layout.txt	2011-12-16 20:39:26 +0000
@@ -177,34 +177,6 @@
     Get the code! https://dev.launchpad.net/
 
 
-Page registering
-----------------
-
-The 'registering' slot is presented on the right side of the 'heading'
-and can be filled with the context registering information (registrant
-and date_created, normally).
-
-    >>> class RegisteringView(LaunchpadView):
-    ...     """A simple view to test base-layout."""
-    ...     __launchpad_facetname__ = 'overview'
-    ...     template = ViewPageTemplateFile('testfiles/main-registering.pt')
-    ...     page_title = 'Test base-layout: main_registering'
-
-    >>> view = RegisteringView(user, request)
-    >>> html = view.render()
-
-    >>> from canonical.launchpad.testing.pages import (
-    ...     first_tag_by_class)
-
-    >>> print first_tag_by_class(html, 'registering')
-    <div id="registration" class="registering">
-    <p>something nice about the context registering.</p>
-    </div>
-
-Note that the slot itself will be already 'styled' and it rarely has
-to be changed.
-
-
 Public and private presentation
 -------------------------------
 

=== modified file 'lib/lp/app/browser/tests/test_base_layout.py'
--- lib/lp/app/browser/tests/test_base_layout.py	2010-11-05 17:24:55 +0000
+++ lib/lp/app/browser/tests/test_base_layout.py	2011-12-16 20:39:26 +0000
@@ -16,11 +16,18 @@
 from BeautifulSoup import BeautifulSoup
 from z3c.ptcompat import ViewPageTemplateFile
 
-from canonical.launchpad.testing.pages import find_tag_by_id
+from canonical.launchpad.testing.pages import (
+    extract_text,
+    find_tag_by_id,
+    )
 from canonical.launchpad.webapp.publisher import LaunchpadView
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.testing import TestCaseWithFactory
+from lp.registry.interfaces.person import PersonVisibility
+from lp.testing import (
+    person_logged_in,
+    TestCaseWithFactory,
+    )
 
 
 class TestBaseLayout(TestCaseWithFactory):
@@ -34,8 +41,9 @@
             SERVER_URL='http://launchpad.dev',
             PATH_INFO='/~waffles/+layout')
         self.request.setPrincipal(self.user)
+        self.context = None
 
-    def makeTemplateView(self, layout):
+    def makeTemplateView(self, layout, context=None):
         """Return a view that uses the specified layout."""
 
         class TemplateView(LaunchpadView):
@@ -45,7 +53,11 @@
                 'testfiles/%s.pt' % layout.replace('_', '-'))
             page_title = 'Test base-layout: %s' % layout
 
-        return TemplateView(self.user, self.request)
+        if context is None:
+            self.context = self.user
+        else:
+            self.context = context
+        return TemplateView(self.context, self.request)
 
     def test_base_layout_doctype(self):
         # Verify that the document is a html DOCTYPE.
@@ -94,7 +106,10 @@
         yui_layout = document.find('div', 'yui-d0')
         watermark = yui_layout.find(True, id='watermark')
         self.assertEqual('watermark-apps-portlet', watermark['class'])
-        self.assertEqual('/@@/person-logo', watermark.img['src'])
+        if self.context.is_team:
+            self.assertEqual('/@@/team-logo', watermark.img['src'])
+        else:
+            self.assertEqual('/@@/person-logo', watermark.img['src'])
         self.assertEqual('Waffles', watermark.h2.string)
         self.assertEqual('facetmenu', watermark.ul['class'])
 
@@ -102,6 +117,7 @@
         # The main_side layout has everything.
         view = self.makeTemplateView('main_side')
         content = BeautifulSoup(view())
+        self.assertIsNot(None, content.find(text=' Extra head content '))
         self.verify_base_layout_html_element(content)
         self.verify_base_layout_head_parts(view, content)
         document = find_tag_by_id(content, 'document')
@@ -110,6 +126,15 @@
         self.assertEqual(classes, document['class'].split())
         self.verify_watermark(document)
         self.assertEqual(
+            'registering', document.find(True, id='registration')['class'])
+        self.assertEqual(
+            'Registered on 2005-09-16 by Illuminati',
+            document.find(True, id='registration').string.strip(),
+            )
+        self.assertEndsWith(
+            extract_text(document.find(True, id='maincontent')),
+            'Main content of the page.')
+        self.assertEqual(
             'yui-b side', document.find(True, id='side-portlets')['class'])
         self.assertEqual('form', document.find(True, id='globalsearch').name)
 
@@ -177,3 +202,29 @@
         footer = find_tag_by_id(content, 'footer')
         link = footer.find('a', text='Contact Launchpad Support').parent
         self.assertEqual('/feedback', link['href'])
+
+    def test_user_without_launchpad_view(self):
+        # When the user does not have launchpad.View on the context,
+        # base-layout does not render the main slot and side slot.
+        owner = self.factory.makePerson()
+        with person_logged_in(owner):
+            team = self.factory.makeTeam(
+                displayname='Waffles', owner=owner,
+                visibility=PersonVisibility.PRIVATE)
+            archive = self.factory.makeArchive(private=True, owner=team)
+            archive.newSubscription(self.user, registrant=owner)
+        with person_logged_in(self.user):
+            view = self.makeTemplateView('main_side', context=team)
+            content = BeautifulSoup(view())
+        self.assertIs(None, content.find(text=' Extra head content '))
+        self.verify_base_layout_html_element(content)
+        self.verify_base_layout_head_parts(view, content)
+        document = find_tag_by_id(content, 'document')
+        self.verify_base_layout_body_parts(document)
+        self.verify_watermark(document)
+        # These parts are unique to the case without launchpad.View.
+        self.assertIsNone(document.find(True, id='side-portlets'))
+        self.assertIsNone(document.find(True, id='registration'))
+        self.assertEndsWith(
+            extract_text(document.find(True, id='maincontent')),
+            'The information in this page is not shared with you.')

=== modified file 'lib/lp/app/browser/tests/test_page_macro.py'
--- lib/lp/app/browser/tests/test_page_macro.py	2010-09-28 14:54:01 +0000
+++ lib/lp/app/browser/tests/test_page_macro.py	2011-12-16 20:39:26 +0000
@@ -6,26 +6,162 @@
 __metaclass__ = type
 
 import os
-
-from zope.component import getMultiAdapter
+from zope.interface import implements
+from zope.location.interfaces import LocationError
 from zope.traversing.interfaces import IPathAdapter
 
-from canonical.launchpad.webapp.publisher import rootObject
-from canonical.launchpad.webapp.servers import LaunchpadTestRequest
-from canonical.testing.layers import FunctionalLayer
-from lp.testing import TestCase
-
-
-class TestPageMacroDispatcher(TestCase):
+from canonical.launchpad.interfaces.launchpad import IPrivacy
+from canonical.testing.layers import (
+    DatabaseFunctionalLayer,
+    FunctionalLayer,
+    )
+from lp.app.security import AuthorizationBase
+from lp.testing import (
+    FakeAdapterMixin,
+    login_person,
+    TestCase,
+    TestCaseWithFactory,
+    test_tales,
+    )
+from lp.testing.views import create_view
+
+
+class ITest(IPrivacy):
+    """A mechanism for adaption."""
+
+
+class TestObject:
+    implements(ITest)
+
+    def __init__(self):
+        self.private = False
+
+
+class TestView:
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+
+class TestPageMacroDispatcherMixin(FakeAdapterMixin):
+
+    def _setUpView(self):
+        self.registerBrowserViewAdapter(TestView, ITest, '+index')
+        self.view = create_view(TestObject(), name='+index')
+
+    def _call_test_tales(self, path):
+        test_tales(path, view=self.view)
+
+
+class PageMacroDispatcherTestCase(TestPageMacroDispatcherMixin, TestCase):
 
     layer = FunctionalLayer
 
+    def setUp(self):
+        super(PageMacroDispatcherTestCase, self).setUp()
+        self._setUpView()
+
     def test_base_template(self):
         # Requests on the launchpad.dev vhost use the Launchpad base template.
-        root_view = getMultiAdapter(
-            (rootObject, LaunchpadTestRequest()), name='index.html')
-        adapter = getMultiAdapter([root_view], IPathAdapter, name='macro')
+        adapter = self.getAdapter([self.view], IPathAdapter, name='macro')
         template_path = os.path.normpath(adapter.base.filename)
         self.assertIn('lp/app/templates', template_path)
         # The base template defines a 'master' macro as the adapter expects.
         self.assertIn('master', adapter.base.macros.keys())
+
+    def test_page(self):
+        # A view can be adpated to a page macro object.
+        page_macro = test_tales('view/macro:page/main_side', view=self.view)
+        self.assertEqual('main_side', self.view.__pagetype__)
+        self.assertEqual(('mode', 'html'), page_macro[1])
+        source_file = page_macro[3]
+        self.assertEqual('setSourceFile', source_file[0])
+        self.assertEqual(
+            '/templates/base-layout.pt', source_file[1].split('..')[1])
+
+    def test_page_unknown_type(self):
+        # An error is raised of the pagetype is not defined.
+        self.assertRaisesWithContent(
+            LocationError, "'unknown pagetype: not-defined'",
+            self._call_test_tales, 'view/macro:page/not-defined')
+
+    def test_pagetype(self):
+        # The pagetype is 'unset', until macro:page is called.
+        self.assertIs(None, getattr(self.view, '__pagetype__', None))
+        self.assertEqual(
+            'unset', test_tales('view/macro:pagetype', view=self.view))
+        test_tales('view/macro:page/main_side', view=self.view)
+        self.assertEqual('main_side', self.view.__pagetype__)
+        self.assertEqual(
+            'main_side', test_tales('view/macro:pagetype', view=self.view))
+
+    def test_pagehas(self):
+        # After the page type is set, the page macro can be queried
+        # for what LayoutElements it supports supports.
+        test_tales('view/macro:page/main_side', view=self.view)
+        self.assertTrue(
+            test_tales('view/macro:pagehas/portlets', view=self.view))
+
+    def test_pagehas_unset_pagetype(self):
+        # The page macro type must be set before the page macro can be
+        # queried for what LayoutElements it supports.
+        self.assertRaisesWithContent(
+            KeyError, "'unset'",
+            self._call_test_tales, 'view/macro:pagehas/fnord')
+
+    def test_pagehas_unknown_attribute(self):
+        # An error is raised if the LayoutElement does not exist.
+        test_tales('view/macro:page/main_side', view=self.view)
+        self.assertRaisesWithContent(
+            KeyError, "'fnord'",
+            self._call_test_tales, 'view/macro:pagehas/fnord')
+
+
+class PageMacroDispatcherInteractionTestCase(TestPageMacroDispatcherMixin,
+                                             TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(PageMacroDispatcherInteractionTestCase, self).setUp()
+        self._setUpView()
+        login_person(self.factory.makePerson())
+
+    def _setUpPermissions(self, has_permission=True):
+        # Setup a specific permission for the test object.
+        class FakeSecurityChecker(AuthorizationBase):
+            """A class to instrument a specific permission."""
+            @classmethod
+            def __call__(adaptee):
+                return FakeSecurityChecker(adaptee)
+
+            def __init__(self, adaptee=None):
+                super(FakeSecurityChecker, self).__init__(adaptee)
+
+            def checkUnauthenticated(self):
+                return has_permission
+
+            def checkAuthenticated(self, user):
+                return has_permission
+
+        self.registerAuthorizationAdapter(
+            FakeSecurityChecker, ITest, 'launchpad.View')
+
+    def test_is_page_contentless_public(self):
+        # Public objects always have content to be shown.
+        self.assertFalse(
+            test_tales('view/macro:is-page-contentless', view=self.view))
+
+    def test_is_page_contentless_private_with_view(self):
+        # Private objects the user can view have content to be shown.
+        self.view.context.private = True
+        self._setUpPermissions(has_permission=True)
+        result = test_tales('view/macro:is-page-contentless', view=self.view)
+        self.assertFalse(result)
+
+    def test_is_page_contentless_private_without_view(self):
+        # Private objects the view cannot view cannot show content.
+        self.view.context.private = True
+        self._setUpPermissions(has_permission=False)
+        result = test_tales('view/macro:is-page-contentless', view=self.view)
+        self.assertTrue(result)

=== removed file 'lib/lp/app/browser/tests/testfiles/main-registering.pt'
--- lib/lp/app/browser/tests/testfiles/main-registering.pt	2009-09-15 22:12:44 +0000
+++ lib/lp/app/browser/tests/testfiles/main-registering.pt	1970-01-01 00:00:00 +0000
@@ -1,19 +0,0 @@
-<html
-  xmlns="http://www.w3.org/1999/xhtml";
-  xmlns:tal="http://xml.zope.org/namespaces/tal";
-  xmlns:metal="http://xml.zope.org/namespaces/metal";
-  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
-  metal:use-macro="view/macro:page/main_only">
-
-  <body>
-
-    <tal:registering metal:fill-slot="registering">
-      <p>something nice about the context registering.</p>
-    </tal:registering>
-
-    <tal:main metal:fill-slot="main">
-     ANYTHING
-    </tal:main>
-
-  </body>
-</html>

=== modified file 'lib/lp/app/browser/tests/testfiles/main-side.pt'
--- lib/lp/app/browser/tests/testfiles/main-side.pt	2009-08-05 19:34:07 +0000
+++ lib/lp/app/browser/tests/testfiles/main-side.pt	2011-12-16 20:39:26 +0000
@@ -13,12 +13,13 @@
 
   <body>
     <tal:heading metal:fill-slot="heading"><h2>Heading</h2></tal:heading>
+
+    <tal:registering metal:fill-slot="registering">
+      Registered on 2005-09-16 by Illuminati
+    </tal:registering>
+
     <tal:main metal:fill-slot="main">
       <div class="top-portlet">
-        <p class="registered">
-          Registered on 2005-09-16
-          by <a class="sprite team" href="#">Illuminati team</a>
-        </p>
         <p>
           Main content of the page.
         </p>

=== removed file 'lib/lp/app/doc/tales-macro.txt'
--- lib/lp/app/doc/tales-macro.txt	2011-07-21 22:45:05 +0000
+++ lib/lp/app/doc/tales-macro.txt	1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
-= The macro: TALES namespace =
-
-Launchpad has a 'macro:' TALES namespace that offers controls over the
-layout of the page.
-
-    >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
-    >>> class FakeView(object):
-    ...     request = LaunchpadTestRequest()
-
-Templates should start by specifying the kind of pagetype they use.
-That's done by using the 'macro:page' traversal. That expression returns
-the master macro from the main_template.pt and sets on the view the
-layout it's using. The following METAL fragment illustrates the way it's
-usually done:
-
-    <html metal:use-macro="view/macro:page/main_side" />
-
-
-    >>> from lp.testing import test_tales
-    >>> view = FakeView()
-
-    # Return value is the compiled macro expression.
-    >>> test_tales('view/macro:page/main_side', view=view)
-    [('version', ...]
-
-The pagetype is registered in the __pagetype__ attribute.
-
-    >>> view.__pagetype__
-    'main_side'
-
-If the pagetype isn't defined, a LocationError is raised.
-
-    >>> test_tales('view/macro:page/not-defined', view=FakeView())
-    Traceback (most recent call last):
-      ...
-    LocationError: 'unknown pagetype: not-defined'
-
-The 'macro:pagehas' can then be used to test for features that should be
-rendered in the layout.
-
-    >>> test_tales('view/macro:pagehas/applicationtabs', view=view)
-    True
-
-    >>> test_tales('view/macro:pagehas/portlets', view=view)
-    True

=== modified file 'lib/lp/app/templates/base-layout.pt'
--- lib/lp/app/templates/base-layout.pt	2011-12-14 08:13:46 +0000
+++ lib/lp/app/templates/base-layout.pt	2011-12-16 20:39:26 +0000
@@ -55,13 +55,17 @@
     </tal:comment>
     <noscript></noscript>
 
-    <meta name="description"
-      tal:condition="view/page_description | nothing"
-      tal:attributes="content view/page_description/fmt:strip-email/fmt:shorten/500" />
+    <tal:view condition="not: view/macro:is-page-contentless">
+      <meta name="description"
+        tal:condition="view/page_description | nothing"
+        tal:attributes="content view/page_description/fmt:strip-email/fmt:shorten/500" />
+    </tal:view>
 
     <metal:page-javascript
         use-macro="context/@@+base-layout-macros/page-javascript" />
-    <metal:block define-slot="head_epilogue"></metal:block>
+    <tal:view condition="not: view/macro:is-page-contentless">
+      <metal:block define-slot="head_epilogue"></metal:block>
+    </tal:view>
   </head>
 
   <body id="document"
@@ -117,7 +121,8 @@
               <tal:breadcrumbs replace="structure context/@@+hierarchy">
                 ProjectName > Branches > Merge Proposals > fix-for-navigation
               </tal:breadcrumbs>
-              <div id="registration" class="registering">
+              <div id="registration" class="registering"
+                tal:condition="not: view/macro:is-page-contentless">
                 <metal:registering define-slot="registering" />
               </div>
             </div>
@@ -132,14 +137,22 @@
                   use-macro="context/@@+base-layout-macros/notifications"/>
               </tal:notifications>
             </div>
-            <metal:main define-slot="main" />
+
+            <tal:view condition="not: view/macro:is-page-contentless">
+              <metal:main define-slot="main" />
+            </tal:view>
+            <tal:limitedview condition="view/macro:is-page-contentless">
+              The information in this page is not shared with you.
+            </tal:limitedview>
           </div><!-- yui-b -->
         </div><!-- yui-main -->
 
-        <div id="side-portlets" class="yui-b side"
-          tal:condition="view/macro:pagehas/portlets">
-          <metal:portlets define-slot="side" />
-        </div><!-- yui-b side -->
+        <tal:view condition="not: view/macro:is-page-contentless">
+          <div id="side-portlets" class="yui-b side"
+            tal:condition="view/macro:pagehas/portlets">
+            <metal:portlets define-slot="side" />
+          </div><!-- yui-b side -->
+        </tal:view>
       </div><!-- yui-t4 -->
 
       <metal:footer
@@ -187,7 +200,7 @@
 </tal:template>
 
 
-<metal:debug-timeline 
+<metal:debug-timeline
   use-macro="context/@@+base-layout-macros/debug-timeline" />
 
 <tal:comment

=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py	2011-10-04 08:46:11 +0000
+++ lib/lp/registry/browser/tests/test_team.py	2011-12-16 20:39:26 +0000
@@ -6,9 +6,14 @@
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
+from canonical.launchpad.testing.pages import (
+    extract_text,
+    find_tag_by_id,
+    )
 from canonical.launchpad.webapp.publisher import canonical_url
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.registry.browser.team import TeamOverviewMenu
+from lp.registry.interfaces.person import PersonVisibility
 from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
 from lp.registry.interfaces.teammembership import (
     ITeamMembershipSet,
@@ -183,3 +188,24 @@
             'in a few minutes.')
         self.assertEqual(1, len(notifications))
         self.assertEqual(message, notifications[0].message)
+
+    def test_user_without_launchpad_view(self):
+        # When the user does not have launchpad.View on the context,
+        user = self.factory.makePerson()
+        owner = self.factory.makePerson()
+        with person_logged_in(owner):
+            team = self.factory.makeTeam(
+                displayname='Waffles', owner=owner,
+                visibility=PersonVisibility.PRIVATE)
+            archive = self.factory.makeArchive(private=True, owner=team)
+            archive.newSubscription(user, registrant=owner)
+        with person_logged_in(user):
+            view = create_initialized_view(
+                team, name="+index",  server_url=canonical_url(team),
+                path_info='', principal=user)
+            document = find_tag_by_id(view(), 'document')
+        self.assertIsNone(document.find(True, id='side-portlets'))
+        self.assertIsNone(document.find(True, id='registration'))
+        self.assertEndsWith(
+            extract_text(document.find(True, id='maincontent')),
+            'The information in this page is not shared with you.')

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-12-11 15:48:11 +0000
+++ lib/lp/testing/__init__.py	2011-12-16 20:39:26 +0000
@@ -17,6 +17,7 @@
     'celebrity_logged_in',
     'ExpectedException',
     'extract_lp_cache',
+    'FakeAdapterMixin',
     'FakeLaunchpadRequest',
     'FakeTime',
     'get_lsb_information',
@@ -100,9 +101,15 @@
 from testtools.matchers import MatchesRegex
 from testtools.testcase import ExpectedException as TTExpectedException
 import transaction
-from zope.component import getUtility
+from zope.component import (
+    getSiteManager,
+    getMultiAdapter,
+    getUtility,
+    )
 import zope.event
+from zope.interface import Interface
 from zope.interface.verify import verifyClass
+from zope.publisher.interfaces.browser import IBrowserRequest
 from zope.security.proxy import (
     isinstance as zope_isinstance,
     removeSecurityProxy,
@@ -126,6 +133,7 @@
     StepsToGo,
     WebServiceTestRequest,
     )
+from lp.app.interfaces.security import IAuthorization
 from lp.codehosting.vfs import (
     branch_id_to_path,
     get_rw_server,
@@ -1429,3 +1437,47 @@
     def stepstogo(self):
         """See `IBasicLaunchpadRequest`."""
         return StepsToGo(self)
+
+
+class FakeAdapterMixin:
+    """A testcase mixin that helps register/unregister Zope adapters.
+
+    These helper methods simplify the task to registering Zope adapters
+    during the setup of a test and they will be unregistered when the
+    test completes.
+    """
+    def registerAdapter(self, adapter_class, for_interfaces,
+                        provided_interface, name=None):
+        """Register an adapter from the required interfacs to the provided.
+
+        eg. registerAdapter(
+                TestOtherThing, (IThing, ILayer), IOther, name='fnord')
+        """
+        getSiteManager().registerAdapter(
+            adapter_class, for_interfaces, provided_interface, name=name)
+        self.addCleanup(
+            getSiteManager().unregisterAdapter, adapter_class,
+            for_interfaces, provided_interface, name=name)
+
+    def registerAuthorizationAdapter(self, authorization_class,
+                                     for_interface, permission_name):
+        """Register a security checker to test authorisation.
+
+        eg. registerAuthorizationAdapter(
+                TestChecker, IPerson, 'launchpad.View')
+        """
+        self.registerAdapter(
+            authorization_class, (for_interface, ), IAuthorization,
+            name=permission_name)
+
+    def registerBrowserViewAdapter(self, view_class, for_interface, name):
+        """Register a security checker to test authorization.
+
+        eg registerBrowserViewAdapter(TestView, IPerson, '+test-view')
+        """
+        self.registerAdapter(
+            view_class, (for_interface, IBrowserRequest), Interface,
+            name=name)
+
+    def getAdapter(self, for_interfaces, provided_interface, name=None):
+        return getMultiAdapter(for_interfaces, provided_interface, name=name)