Merge lp:~wallyworld/launchpad/recursive-branch-resolution-failure into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: no longer in the source branch.
Merged at revision: 11613
Proposed branch: lp:~wallyworld/launchpad/recursive-branch-resolution-failure
Merge into: lp:launchpad
Diff against target: 168 lines (+68/-15)
2 files modified
lib/canonical/launchpad/browser/launchpad.py (+21/-7)
lib/canonical/launchpad/browser/tests/test_launchpad.py (+47/-8)
To merge this branch: bzr merge lp:~wallyworld/launchpad/recursive-branch-resolution-failure
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+36403@code.launchpad.net

Commit message

When parsing short from branch urls of the for +branch/foo/bar, in the case of an error, handle the case where the referring url is None.

Description of the change

Bug 645544 describes a recursive failure when navigating to a url like +/branch/foo. This is a bug resulting from work done on lp:~wallyworld/lanchpad/tales-linkify-broken-links for bug 404131.

The problem is that when a url is entered directly, the http referer is None and the redirection logic tries to send the browser to an invalid url.

Proposed fix

Simple fix in the error handling logic to simply raise an exception if there is no referer in the http request header. This is how the system used to work before the fix for bug 404131 was done.

Implementation Details

The only code changes were to:
    - lib/canonical/launchpad/browser/launchpad.py
    - lib/canonical/launchpad/browser/tests/test_launchpad.py

Tests

    bin/test -vvm canonical.launchpad.browser.tests.test_launchpad TestBranchTraversal

    New tests were written to test for the cases where the referring url is None:
    - test_nonexistent_product_without_referer
    - test_private_without_referer

The TestBranchTraversal class (and friends) needed to be changed to pass a default referring url to the business logic being tested since previously the referring url was always None. The test cases never made the distinction between navigating from a valid url or hacking the url directly.

lint

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/browser/launchpad.py
  lib/canonical/launchpad/browser/tests/test_launchpad.py

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

As I said on IRC I'd like you to change the name of the 'url' variable redirect_branch to http_referer or referer. Otherwise it's all fine.

Cheers!

review: Approve
Revision history for this message
Ian Booth (wallyworld) wrote :

I noticed there was still a code path were a redirect to None could occur - if an attempt was made to navigate directly to a private branch. Code fixed and new unit tested added.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

The changes look fine too.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
2--- lib/canonical/launchpad/browser/launchpad.py 2010-09-15 06:40:16 +0000
3+++ lib/canonical/launchpad/browser/launchpad.py 2010-09-23 05:22:52 +0000
4@@ -521,33 +521,47 @@
5 with a suitable error message.
6 """
7
8- # The default url to go to will be back to the referring page (in
9- # the case that there is an error resolving the branch url)
10- url = self.request.getHeader('referer')
11+ # The default target url to go to will be back to the referring page
12+ # (in the case that there is an error resolving the branch url).
13+ # Note: the http referer may be None if someone has hacked a url
14+ # directly rather than following a /+branch/<foo> link.
15+ target_url = self.request.getHeader('referer')
16 path = '/'.join(self.request.stepstogo)
17 try:
18 # first check for a valid branch url
19 try:
20 branch_data = getUtility(IBranchLookup).getByLPPath(path)
21 branch, trailing = branch_data
22- url = canonical_url(branch)
23+ target_url = canonical_url(branch)
24 if trailing is not None:
25- url = urlappend(url, trailing)
26+ target_url = urlappend(target_url, trailing)
27
28- except (NoLinkedBranch):
29+ except (NoLinkedBranch) as e:
30 # a valid ICanHasLinkedBranch target exists but there's no
31 # branch or it's not visible
32+
33+ # If are aren't arriving at this invalid branch URL from
34+ # another page then we just raise an exception, otherwise we
35+ # end up in a bad recursion loop. The target url will be None
36+ # in that case.
37+ if target_url is None:
38+ raise e
39 self.request.response.addNotification(
40 "The target %s does not have a linked branch." % path)
41
42 except (CannotHaveLinkedBranch, InvalidNamespace,
43 InvalidProductName, NotFoundError) as e:
44+ # If are aren't arriving at this invalid branch URL from another
45+ # page then we just raise an exception, otherwise we end up in a
46+ # bad recursion loop. The target url will be None in that case.
47+ if target_url is None:
48+ raise e
49 error_msg = str(e)
50 if error_msg == '':
51 error_msg = "Invalid branch lp:%s." % path
52 self.request.response.addErrorNotification(error_msg)
53
54- return self.redirectSubTree(url)
55+ return self.redirectSubTree(target_url)
56
57 @stepto('+builds')
58 def redirect_buildfarm(self):
59
60=== modified file 'lib/canonical/launchpad/browser/tests/test_launchpad.py'
61--- lib/canonical/launchpad/browser/tests/test_launchpad.py 2010-09-16 06:02:36 +0000
62+++ lib/canonical/launchpad/browser/tests/test_launchpad.py 2010-09-23 05:22:52 +0000
63@@ -20,6 +20,7 @@
64 from canonical.launchpad.webapp.url import urlappend
65 from canonical.testing.layers import DatabaseFunctionalLayer
66 from lp.app.errors import GoneError
67+from lp.code.errors import NoLinkedBranch
68 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
69 from lp.registry.interfaces.person import (
70 IPersonSet,
71@@ -33,6 +34,11 @@
72 from lp.testing.views import create_view
73
74
75+# We set the request header HTTP_REFERER when we want to simulate navigation
76+# from a valid page. This is used in the assertDisplaysNotification check.
77+DEFAULT_REFERER = 'http://launchpad.dev'
78+
79+
80 class TraversalMixin:
81
82 def _validateNotificationContext(
83@@ -73,28 +79,40 @@
84 """
85
86 redirection = self.traverse(path)
87- self.assertIs(redirection.target, None)
88+ self.assertIs(redirection.target, DEFAULT_REFERER)
89 self._validateNotificationContext(
90 redirection.request, notification, level)
91
92- def assertNotFound(self, path):
93- self.assertRaises(NotFound, self.traverse, path)
94+ def assertNoLinkedBranch(self, path, use_default_referer=True):
95+ self.assertRaises(
96+ NoLinkedBranch, self.traverse, path,
97+ use_default_referer=use_default_referer)
98+
99+ def assertNotFound(self, path, use_default_referer=True):
100+ self.assertRaises(
101+ NotFound, self.traverse, path,
102+ use_default_referer=use_default_referer)
103
104 def assertRedirects(self, segments, url):
105 redirection = self.traverse(segments)
106 self.assertEqual(url, redirection.target)
107
108- def traverse(self, path, first_segment):
109+ def traverse(self, path, first_segment, use_default_referer=True):
110 """Traverse to 'segments' using a 'LaunchpadRootNavigation' object.
111
112 Using the Zope traversal machinery, traverse to the path given by
113 'segments', starting at a `LaunchpadRootNavigation` object.
114
115 :param segments: A list of path segments.
116+ :param use_default_referer: If True, set the referer attribute in the
117+ request header to DEFAULT_REFERER = "http://launchpad.dev"
118+ (otherwise it remains as None)
119 :return: The object found.
120 """
121- request = LaunchpadTestRequest(
122- PATH_INFO=urlappend('/%s' % first_segment, path))
123+ extra = {'PATH_INFO': urlappend('/%s' % first_segment, path)}
124+ if use_default_referer:
125+ extra['HTTP_REFERER'] = DEFAULT_REFERER
126+ request = LaunchpadTestRequest(**extra)
127 segments = reversed(path.split('/'))
128 request.setTraversalStack(segments)
129 traverser = LaunchpadRootNavigation(None, request=request)
130@@ -110,8 +128,9 @@
131
132 layer = DatabaseFunctionalLayer
133
134- def traverse(self, path):
135- return super(TestBranchTraversal, self).traverse(path, '+branch')
136+ def traverse(self, path, **kwargs):
137+ return super(TestBranchTraversal, self).traverse(
138+ path, '+branch', **kwargs)
139
140 def test_unique_name_traversal(self):
141 # Traversing to /+branch/<unique_name> redirects to the page for that
142@@ -178,6 +197,26 @@
143 non_existent, requiredMessage,
144 BrowserNotificationLevel.ERROR)
145
146+ def test_nonexistent_product_without_referer(self):
147+ # Traversing to /+branch/<no-such-product> without a referer results
148+ # in a 404 error. This happens if the user hacks the URL rather than
149+ # navigating via a link
150+ self.assertNotFound('non-existent', use_default_referer=False)
151+
152+ def test_private_without_referer(self):
153+ # If the development focus of a product is private and there is no
154+ # referer, we will get a 404 error. This happens if the user hacks
155+ # the URL rather than navigating via a link
156+ branch = self.factory.makeProductBranch()
157+ naked_product = removeSecurityProxy(branch.product)
158+ ICanHasLinkedBranch(naked_product).setBranch(branch)
159+ removeSecurityProxy(branch).private = True
160+
161+ any_user = self.factory.makePerson()
162+ login_person(any_user)
163+ self.assertNoLinkedBranch(
164+ naked_product.name, use_default_referer=False)
165+
166 def test_product_without_dev_focus(self):
167 # Traversing to a product without a development focus displays a
168 # user message on the same page.