Merge ~cjwatson/launchpad:zope.testbrowser-5 into launchpad:master

Proposed by Colin Watson on 2019-11-12
Status: Merged
Approved by: Colin Watson on 2019-11-22
Approved revision: 77c4f0dd1f2d45c3643ec73c5e5b8c27d64cd533
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:zope.testbrowser-5
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:zope.testbrowser-5-prepare
Diff against target: 1370 lines (+239/-196)
47 files modified
constraints.txt (+1/-1)
lib/launchpad_loggerhead/tests.py (+0/-3)
lib/lp/answers/stories/question-search-multiple-languages.txt (+0/-8)
lib/lp/app/__init__.py (+2/-5)
lib/lp/app/stories/basics/xx-offsite-form-post.txt (+28/-9)
lib/lp/blueprints/browser/tests/test_sprint.py (+1/-1)
lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt (+1/-2)
lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt (+1/-1)
lib/lp/buildmaster/stories/xx-builder-page.txt (+0/-3)
lib/lp/buildmaster/stories/xx-buildfarm-index.txt (+0/-2)
lib/lp/code/browser/tests/test_gitsubscription.py (+1/-1)
lib/lp/code/browser/tests/test_product.py (+1/-1)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+1/-1)
lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py (+5/-8)
lib/lp/code/stories/branches/xx-branch-edit.txt (+1/-1)
lib/lp/code/stories/branches/xx-branch-listings.txt (+1/-1)
lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt (+3/-3)
lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt (+0/-1)
lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt (+0/-4)
lib/lp/registry/stories/milestone/object-milestones.txt (+2/-2)
lib/lp/registry/stories/team/xx-team-membership.txt (+3/-3)
lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt (+3/-3)
lib/lp/registry/stories/teammembership/xx-teammembership.txt (+1/-1)
lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt (+1/-1)
lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt (+5/-8)
lib/lp/services/webapp/tests/test_login.py (+11/-16)
lib/lp/snappy/browser/tests/test_snap.py (+13/-16)
lib/lp/snappy/browser/tests/test_snapbuild.py (+5/-11)
lib/lp/soyuz/browser/tests/test_archive.py (+3/-2)
lib/lp/soyuz/browser/tests/test_livefs.py (+1/-1)
lib/lp/soyuz/browser/tests/test_livefsbuild.py (+5/-11)
lib/lp/soyuz/stories/ppa/xx-delete-packages.txt (+8/-6)
lib/lp/soyuz/stories/ppa/xx-ppa-files.txt (+1/-1)
lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt (+1/-2)
lib/lp/soyuz/stories/soyuz/xx-private-builds.txt (+0/-5)
lib/lp/testing/__init__.py (+1/-2)
lib/lp/testing/doc/pagetest-helpers.txt (+1/-1)
lib/lp/testing/layers.py (+11/-8)
lib/lp/testing/pages.py (+85/-5)
lib/lp/translations/stories/buildfarm/xx-build-summary.txt (+0/-1)
lib/lp/translations/stories/standalone/xx-pofile-translate-alternative-language.txt (+13/-7)
lib/lp/translations/stories/standalone/xx-pofile-translate-lang-direction.txt (+10/-10)
lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt (+2/-1)
lib/lp/translations/stories/standalone/xx-sourcepackage-export.txt (+1/-1)
lib/lp/translations/stories/translationgroups/xx-translationgroups.txt (+0/-6)
setup.py (+0/-1)
utilities/paste (+5/-8)
Reviewer Review Type Date Requested Status
Ioana Lasc 2019-11-12 Approve on 2019-11-22
Review via email: mp+375427@code.launchpad.net

Commit message

Upgrade to zope.testbrowser 5.5.1

zope.testbrowser 5.0.0 switched its internal implementation to WebTest
instead of mechanize. This necessitates several changes in Launchpad.
In some cases the new default behaviours are already appropriate (for
example, `<meta http-equiv="refresh" />` tags are no longer followed),
and in some we just need to poke into the implementation in slightly
different ways.

We have to patch around a few bugs, although fortunately this can all be
contained in lp.testing.pages:

 * WebTest doesn't understand `<input type="search" />`
   (https://github.com/Pylons/webtest/pull/219, awaiting an upstream
   release).

 * `Browser.reload` reuses the existing request rather than making a new
   one (related to
   https://github.com/zopefoundation/zope.testbrowser/issues/74).

 * zope.testbrowser doesn't support finding links by image alt text.

Description of the change

To post a comment you must log in.
Ioana Lasc (ilasc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/constraints.txt b/constraints.txt
2index 9bcb49a..94b80d3 100644
3--- a/constraints.txt
4+++ b/constraints.txt
5@@ -151,7 +151,7 @@ zope.app.publication==3.12.0
6 #zope.app.wsgi==3.10.0
7 zope.app.wsgi==3.15.0
8 #zope.testbrowser==3.10.4
9-zope.testbrowser[wsgi]==4.0.4
10+zope.testbrowser[wsgi]==5.5.1
11
12 # Deprecated
13 roman==1.4.0
14diff --git a/lib/launchpad_loggerhead/tests.py b/lib/launchpad_loggerhead/tests.py
15index 31cb5a7..7eae09a 100644
16--- a/lib/launchpad_loggerhead/tests.py
17+++ b/lib/launchpad_loggerhead/tests.py
18@@ -74,9 +74,6 @@ class TestLogout(TestCase):
19 app = SessionHandler(app, SESSION_VAR, SECRET)
20 self.cookie_name = app.cookie_handler.cookie_name
21 self.browser = Browser(wsgi_app=app)
22- # We want to pretend we are not a robot, or else mechanize will honor
23- # robots.txt.
24- self.browser.mech_browser.set_handle_robots(False)
25 self.browser.open(
26 config.codehosting.secure_codebrowse_root + '+login')
27
28diff --git a/lib/lp/answers/stories/question-search-multiple-languages.txt b/lib/lp/answers/stories/question-search-multiple-languages.txt
29index b6353d2..e500547 100644
30--- a/lib/lp/answers/stories/question-search-multiple-languages.txt
31+++ b/lib/lp/answers/stories/question-search-multiple-languages.txt
32@@ -118,10 +118,6 @@ The mozilla-firefox sourcepackage only has English questions. When the
33 anonymous user makes a request from a GeoIP that has no languages
34 mapped, we assume they speak the default language of English.
35
36- >>> for pos, (key, _) in enumerate(anon_browser.mech_browser.addheaders):
37- ... if key == 'X_FORWARDED_FOR':
38- ... del anon_browser.mech_browser.addheaders[pos]
39- ... break
40 >>> anon_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1')
41 >>> anon_browser.open(
42 ... 'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions')
43@@ -175,10 +171,6 @@ languages and the target's question languages is 'en'.
44 When the project languages are just English, and the user speaks
45 that language, we do not show the language controls.
46
47- >>> for pos, (key, _) in enumerate(user_browser.mech_browser.addheaders):
48- ... if key == 'X_FORWARDED_FOR':
49- ... del user_browser.mech_browser.addheaders[pos]
50- ... break
51 >>> user_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1')
52 >>> user_browser.open(
53 ... 'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions')
54diff --git a/lib/lp/app/__init__.py b/lib/lp/app/__init__.py
55index 0a4887f..fc252a8 100644
56--- a/lib/lp/app/__init__.py
57+++ b/lib/lp/app/__init__.py
58@@ -19,13 +19,10 @@ from zope.formlib import itemswidgets
59
60 itemswidgets.EXPLICIT_EMPTY_SELECTION = False
61
62-# Monkeypatch our embedded BeautifulSoup and the one in mechanize to
63-# teach them that wbr (new in HTML5, but widely supported forever) is
64-# self-closing.
65+# Monkeypatch our embedded BeautifulSoup to teach it that wbr (new in HTML5,
66+# but widely supported forever) is self-closing.
67 import BeautifulSoup
68-import mechanize._beautifulsoup
69 BeautifulSoup.BeautifulSoup.SELF_CLOSING_TAGS['wbr'] = None
70-mechanize._beautifulsoup.BeautifulSoup.SELF_CLOSING_TAGS['wbr'] = None
71
72 # Load versioninfo.py so that we get errors on start-up rather than waiting
73 # for first page load.
74diff --git a/lib/lp/app/stories/basics/xx-offsite-form-post.txt b/lib/lp/app/stories/basics/xx-offsite-form-post.txt
75index 6924034..74b9812 100644
76--- a/lib/lp/app/stories/basics/xx-offsite-form-post.txt
77+++ b/lib/lp/app/stories/basics/xx-offsite-form-post.txt
78@@ -4,17 +4,36 @@ Referrer Checking on Form Posts
79 To help mitigate cross site request forgery attacks, we check that the
80 referrer for a form post exists and is a URI from one of the Launchpad sites.
81
82-First a helper to set up a browser object that doesn't handle the
83-"Referer" header automatically. We need to poke into the internal
84-mechanize browser object here because Zope's test browser does not
85-expose the required functionality:
86+First a helper to set up a browser object that doesn't handle the "Referer"
87+header automatically. We need to poke into the internals of
88+zope.testbrowser.browser.Browser here because it doesn't expose the required
89+functionality:
90+
91+ >>> from contextlib import contextmanager
92+ >>> from lp.testing.pages import Browser
93+
94+ >>> class BrowserWithReferrer(Browser):
95+ ... def __init__(self, referrer):
96+ ... self._referrer = referrer
97+ ... super(BrowserWithReferrer, self).__init__()
98+ ...
99+ ... @contextmanager
100+ ... def _preparedRequest(self, url):
101+ ... with super(BrowserWithReferrer, self)._preparedRequest(
102+ ... url) as reqargs:
103+ ... reqargs["headers"] = [
104+ ... (key, value) for key, value in reqargs["headers"]
105+ ... if key.lower() != "referer"]
106+ ... if self._referrer is not None:
107+ ... reqargs["headers"].append(("Referer", self._referrer))
108+ ... yield reqargs
109
110 >>> def setupBrowserWithReferrer(referrer):
111- ... browser = setupBrowser("Basic no-priv@canonical.com:test")
112- ... browser.mech_browser.set_handle_referer(False)
113- ... if referrer is not None:
114- ... browser.addHeader("Referer", referrer)
115- ... return browser
116+ ... browser = BrowserWithReferrer(referrer)
117+ ... browser.handleErrors = False
118+ ... browser.addHeader(
119+ ... "Authorization", "Basic no-priv@canonical.com:test")
120+ ... return browser
121
122
123 If we try to create a new team with with the referrer set to
124diff --git a/lib/lp/blueprints/browser/tests/test_sprint.py b/lib/lp/blueprints/browser/tests/test_sprint.py
125index 907e6a5..066d235 100644
126--- a/lib/lp/blueprints/browser/tests/test_sprint.py
127+++ b/lib/lp/blueprints/browser/tests/test_sprint.py
128@@ -8,11 +8,11 @@ from __future__ import absolute_import, print_function, unicode_literals
129 __metaclass__ = type
130
131 from fixtures import FakeLogger
132-from mechanize import LinkNotFoundError
133 from testtools.matchers import Equals
134 from zope.publisher.interfaces import NotFound
135 from zope.security.interfaces import Unauthorized
136 from zope.security.proxy import removeSecurityProxy
137+from zope.testbrowser.browser import LinkNotFoundError
138
139 from lp.app.enums import InformationType
140 from lp.services.webapp.publisher import canonical_url
141diff --git a/lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt b/lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt
142index 268d83d..325d595 100644
143--- a/lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt
144+++ b/lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt
145@@ -119,8 +119,7 @@ any team and hence does no see the option to asign somebody else.
146 >>> assignee_control.value = ["jokosher.assignee.assign_to"]
147 Traceback (most recent call last):
148 ...
149- ItemNotFoundError: insufficient items with name
150- u'jokosher.assignee.assign_to'
151+ ValueError: Option u'jokosher.assignee.assign_to' not found ...
152 >>> user_browser.getControl(name="jokosher.assignee", index=0)
153 Traceback (most recent call last):
154 ...
155diff --git a/lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt b/lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt
156index 3ebc332..76dbf19 100644
157--- a/lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt
158+++ b/lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt
159@@ -12,4 +12,4 @@ option to subscribe to add a new subscription
160 >>> logout()
161 >>> user_browser.open(url)
162 >>> user_browser.getLink("Add a subscription")
163- <Link text='Add a subscription' url='.../+subscriptions#'>
164+ <Link text='Add a subscription' url='.../+subscriptions'>
165diff --git a/lib/lp/buildmaster/stories/xx-builder-page.txt b/lib/lp/buildmaster/stories/xx-builder-page.txt
166index fed6bf4..2eafcbd 100644
167--- a/lib/lp/buildmaster/stories/xx-builder-page.txt
168+++ b/lib/lp/buildmaster/stories/xx-builder-page.txt
169@@ -13,7 +13,6 @@ builder state. In the sampledata, the builder 'bob' is building
170 >>> bob_builder.version = "100"
171 >>> logout()
172
173- >>> anon_browser.mech_browser.set_handle_equiv(False)
174 >>> anon_browser.open("http://launchpad.test/builders")
175 >>> anon_browser.getLink("bob").click()
176
177@@ -37,7 +36,6 @@ When accessed by logged in users, the builder page renders the
178 timezone. This way they can easily find out if they are reading
179 outdated information.
180
181- >>> user_browser.mech_browser.set_handle_equiv(False)
182 >>> user_browser.open(anon_browser.url)
183 >>> print extract_text(find_portlet(
184 ... user_browser.contents, 'View full history Current status'))
185@@ -107,7 +105,6 @@ the builder actions.
186
187 >>> cprov_browser = setupBrowser(
188 ... auth='Basic celso.providelo@canonical.com:test')
189- >>> cprov_browser.mech_browser.set_handle_equiv(False)
190
191 >>> cprov_browser.open('http://launchpad.test/builders')
192 >>> cprov_browser.getLink('bob').click()
193diff --git a/lib/lp/buildmaster/stories/xx-buildfarm-index.txt b/lib/lp/buildmaster/stories/xx-buildfarm-index.txt
194index e1d53d8..4ceebda 100644
195--- a/lib/lp/buildmaster/stories/xx-buildfarm-index.txt
196+++ b/lib/lp/buildmaster/stories/xx-buildfarm-index.txt
197@@ -4,7 +4,6 @@ The BuildFarm page is accessible from the root page, although we don't
198 link to it yet because we are not yet sure of the benefits of doing
199 this, since the audience of this page is still restricted.
200
201- >>> anon_browser.mech_browser.set_handle_equiv(False)
202 >>> anon_browser.open('http://launchpad.test/+builds')
203
204 The BuildFarm contains a list of all builders registered in Launchpad
205@@ -121,7 +120,6 @@ and are not permitted if they go directly to the URL.
206
207 Administrators can create new builders.
208
209- >>> admin_browser.mech_browser.set_handle_equiv(False)
210 >>> admin_browser.open("http://launchpad.test/+builds/+index")
211
212 >>> admin_browser.getLink("Register a new build machine").click()
213diff --git a/lib/lp/code/browser/tests/test_gitsubscription.py b/lib/lp/code/browser/tests/test_gitsubscription.py
214index cb49310..62346ff 100644
215--- a/lib/lp/code/browser/tests/test_gitsubscription.py
216+++ b/lib/lp/code/browser/tests/test_gitsubscription.py
217@@ -10,9 +10,9 @@ __metaclass__ = type
218 from urllib import urlencode
219
220 from fixtures import FakeLogger
221-from mechanize import LinkNotFoundError
222 from testtools.matchers import MatchesStructure
223 from zope.security.interfaces import Unauthorized
224+from zope.testbrowser.browser import LinkNotFoundError
225
226 from lp.app.enums import InformationType
227 from lp.code.enums import (
228diff --git a/lib/lp/code/browser/tests/test_product.py b/lib/lp/code/browser/tests/test_product.py
229index a34bb3b..360745e 100644
230--- a/lib/lp/code/browser/tests/test_product.py
231+++ b/lib/lp/code/browser/tests/test_product.py
232@@ -12,9 +12,9 @@ from datetime import (
233 timedelta,
234 )
235
236-from mechanize import LinkNotFoundError
237 import pytz
238 from zope.component import getUtility
239+from zope.testbrowser.browser import LinkNotFoundError
240
241 from lp.app.enums import (
242 InformationType,
243diff --git a/lib/lp/code/browser/tests/test_sourcepackagerecipe.py b/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
244index 16952c2..c96ac22 100644
245--- a/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
246+++ b/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
247@@ -15,13 +15,13 @@ import re
248 from textwrap import dedent
249
250 from fixtures import FakeLogger
251-from mechanize import LinkNotFoundError
252 from pytz import UTC
253 from testtools.matchers import Equals
254 import transaction
255 from zope.component import getUtility
256 from zope.security.interfaces import Unauthorized
257 from zope.security.proxy import removeSecurityProxy
258+from zope.testbrowser.browser import LinkNotFoundError
259
260 from lp.app.enums import InformationType
261 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
262diff --git a/lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py b/lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py
263index 669a035..3f3ec4f 100644
264--- a/lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py
265+++ b/lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py
266@@ -8,13 +8,13 @@ from __future__ import absolute_import, print_function, unicode_literals
267 __metaclass__ = type
268
269 from fixtures import FakeLogger
270-from mechanize import LinkNotFoundError
271 from storm.locals import Store
272 from testtools.matchers import StartsWith
273 import transaction
274 from zope.component import getUtility
275 from zope.security.interfaces import Unauthorized
276 from zope.security.proxy import removeSecurityProxy
277+from zope.testbrowser.browser import LinkNotFoundError
278
279 from lp.buildmaster.enums import BuildStatus
280 from lp.buildmaster.interfaces.processor import IProcessorSet
281@@ -270,22 +270,19 @@ class TestSourcePackageRecipeBuild(BrowserTestCase):
282
283 def test_builder_index_public(self):
284 build = self.makeBuildingRecipe()
285- url = canonical_url(build.builder)
286- logout()
287- browser = self.getNonRedirectingBrowser(url=url, user=ANONYMOUS)
288+ browser = self.getViewBrowser(build.builder, no_login=True)
289 self.assertIn('i am failing', browser.contents)
290
291 def test_builder_index_private(self):
292 archive = self.factory.makeArchive(private=True)
293 with admin_logged_in():
294 build = self.makeBuildingRecipe(archive=archive)
295- url = canonical_url(removeSecurityProxy(build).builder)
296- logout()
297+ builder = removeSecurityProxy(build).builder
298
299 # An unrelated user can't see the logtail of a private build.
300- browser = self.getNonRedirectingBrowser(url=url)
301+ browser = self.getViewBrowser(builder)
302 self.assertNotIn('i am failing', browser.contents)
303
304 # But someone who can see the archive can.
305- browser = self.getNonRedirectingBrowser(url=url, user=archive.owner)
306+ browser = self.getViewBrowser(builder, user=archive.owner)
307 self.assertIn('i am failing', browser.contents)
308diff --git a/lib/lp/code/stories/branches/xx-branch-edit.txt b/lib/lp/code/stories/branches/xx-branch-edit.txt
309index 1142650..f501da0 100644
310--- a/lib/lp/code/stories/branches/xx-branch-edit.txt
311+++ b/lib/lp/code/stories/branches/xx-branch-edit.txt
312@@ -185,7 +185,7 @@ already own in the same product.
313 >>> browser.getControl('Name').value = '2.6'
314 >>> browser.getControl('Change Branch').click()
315 >>> browser.url
316- 'http://code.launchpad.test/%7Ename12/gnome-terminal/main/+edit'
317+ 'http://code.launchpad.test/~name12/gnome-terminal/main/+edit'
318
319 >>> print_feedback_messages(browser.contents)
320 There is 1 error.
321diff --git a/lib/lp/code/stories/branches/xx-branch-listings.txt b/lib/lp/code/stories/branches/xx-branch-listings.txt
322index 16e18a8..431aa01 100644
323--- a/lib/lp/code/stories/branches/xx-branch-listings.txt
324+++ b/lib/lp/code/stories/branches/xx-branch-listings.txt
325@@ -300,7 +300,7 @@ options that can be selected.
326 >>> sort_by_control.value = ['by project name']
327 Traceback (most recent call last):
328 ...
329- ItemNotFoundError: insufficient items with name u'by project name'
330+ ValueError: Option u'by project name' not found ...
331 >>> for option in sort_by_control.options:
332 ... print(option)
333 by most interesting
334diff --git a/lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt b/lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt
335index f110e32..688e713 100644
336--- a/lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt
337+++ b/lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt
338@@ -50,7 +50,7 @@ read "3 recipes." Let's click through.
339 >>> nopriv_browser.open(branch_url)
340 >>> nopriv_browser.getLink('3 recipes').click()
341 >>> print(nopriv_browser.url)
342- http://code.launchpad.test/%7Eperson-name.../product-name.../branch.../+recipes
343+ http://code.launchpad.test/~person-name.../product-name.../branch.../+recipes
344
345 The "Base Source" column should not be shown.
346
347@@ -98,7 +98,7 @@ should now read "4 recipes." Let's click through.
348 >>> nopriv_browser.open(repository_url)
349 >>> nopriv_browser.getLink('4 recipes').click()
350 >>> print(nopriv_browser.url)
351- http://code.launchpad.test/%7Eperson-name.../product-name.../+git/gitrepository.../+recipes
352+ http://code.launchpad.test/~person-name.../product-name.../+git/gitrepository.../+recipes
353
354 The "Base Source" column should not be shown.
355
356@@ -125,7 +125,7 @@ listed.
357 ... nopriv_browser.open(ref1_url)
358 >>> nopriv_browser.getLink('2 recipes').click()
359 >>> print(nopriv_browser.url)
360- http://code.launchpad.test/%7Eperson-name.../product-name.../+git/gitrepository.../+ref/a/+recipes
361+ http://code.launchpad.test/~person-name.../product-name.../+git/gitrepository.../+ref/a/+recipes
362
363 >>> print_recipe_listing_head(nopriv_browser)
364 Name
365diff --git a/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt b/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt
366index 40ac555..399f15d 100644
367--- a/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt
368+++ b/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt
369@@ -29,7 +29,6 @@ archive mirrors, plus the canonical one.
370 Using a request with no IP address information will give us only the
371 canonical mirror.
372
373- >>> anon_browser.addHeader('X_FORWARDED_FOR', None)
374 >>> anon_browser.open(
375 ... 'http://launchpad.test/ubuntu/+countrymirrors-archive')
376 >>> print anon_browser.headers['X-Generated-For-Country']
377diff --git a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
378index f83f7e5..bcca447 100644
379--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
380+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
381@@ -631,10 +631,6 @@ Test if the advertisement email was sent:
382
383 Let's login with an Launchpad Admin
384
385- >>> for pos, (key, _) in enumerate(browser.mech_browser.addheaders):
386- ... if key == 'Authorization':
387- ... del browser.mech_browser.addheaders[pos]
388- ... break
389 >>> browser.addHeader(
390 ... 'Authorization', 'Basic guilherme.salgado@canonical.com:test')
391
392diff --git a/lib/lp/registry/stories/milestone/object-milestones.txt b/lib/lp/registry/stories/milestone/object-milestones.txt
393index d1ef248..8a3e624 100644
394--- a/lib/lp/registry/stories/milestone/object-milestones.txt
395+++ b/lib/lp/registry/stories/milestone/object-milestones.txt
396@@ -232,8 +232,8 @@ Next, we'll target each bug to the 1.0 milestone:
397 >>> control = browser.getControl('Milestone')
398 >>> milestone_name = '1.0'
399 >>> [milestone_id] = [
400- ... option.name for option in control.mech_control.items
401- ... if option.get_labels()[0].text.endswith(milestone_name)]
402+ ... option.optionValue for option in control.controls
403+ ... if option.labels[0].endswith(milestone_name)]
404 >>> control.value = [milestone_id]
405 >>> browser.getControl('Save Changes').click()
406
407diff --git a/lib/lp/registry/stories/team/xx-team-membership.txt b/lib/lp/registry/stories/team/xx-team-membership.txt
408index a7eca20..f21ee07 100644
409--- a/lib/lp/registry/stories/team/xx-team-membership.txt
410+++ b/lib/lp/registry/stories/team/xx-team-membership.txt
411@@ -36,12 +36,12 @@ which would enable the input when the radio button was clicked to indicate
412 that a specific expiration date was desired. There is also no TestBrowser
413 way to "enable" the input. So, we have to reach into the guts of the
414 TestBrowser to manually re-enable the input. That's what the
415-control.mech_control.disabled=False stuff is.
416+`del expiry._control.attrs['disabled']` stuff is.
417
418 >>> browser.getControl(name='admin').value = ['no']
419 >>> browser.getControl(name='expires').value = ['date']
420 >>> expiry = browser.getControl(name='membership.expirationdate')
421- >>> expiry.mech_control.disabled = False # control may have been disabled
422+ >>> del expiry._control.attrs['disabled']
423 >>> expiry.value = 'ssdf'
424 >>> browser.getControl('Change').click()
425
426@@ -80,7 +80,7 @@ next year -- successfully.
427 >>> browser.getControl(name='admin').value = ['no']
428 >>> browser.getControl(name='expires').value = ['date']
429 >>> expiry = browser.getControl(name='membership.expirationdate')
430- >>> expiry.mech_control.disabled = False # control may have been disabled
431+ >>> del expiry._control.attrs['disabled']
432 >>> expiry.value = expire_date.strftime('%Y-%m-%d')
433 >>> browser.getControl(name='comment').value = 'Arfie'
434 >>> browser.getControl('Change').click()
435diff --git a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
436index c7113c5..bec3b86 100644
437--- a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
438+++ b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
439@@ -81,8 +81,8 @@ the user to renew that membership because it's not about to expire.
440 If we now change Karl's membership to expire in a couple days, he'll be
441 able to renew it himself.
442
443-See pagetests/foaf/40-team-membership.txt for an explanation of the
444-mech_control TestBrowser voodoo.
445+See lib/lp/registry/stories/team/xx-team-membership.txt for an explanation
446+of the expiry._control.attrs TestBrowser voodoo.
447
448 >>> from datetime import datetime, timedelta
449 >>> import pytz
450@@ -95,7 +95,7 @@ mech_control TestBrowser voodoo.
451 >>> team_admin_browser.getControl(name='expires').value = ['date']
452 >>> expiry = team_admin_browser.getControl(
453 ... name='membership.expirationdate')
454- >>> expiry.mech_control.disabled = False # control may be disabled in JS
455+ >>> del expiry._control.attrs['disabled']
456 >>> expiry.value = day_after_tomorrow.date().strftime('%Y-%m-%d')
457 >>> team_admin_browser.getControl(name='change').click()
458
459diff --git a/lib/lp/registry/stories/teammembership/xx-teammembership.txt b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
460index f3f278b..06208ac 100644
461--- a/lib/lp/registry/stories/teammembership/xx-teammembership.txt
462+++ b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
463@@ -415,7 +415,7 @@ error message:
464 ... '2049-04-16'
465 >>> browser2.getControl("Reactivate").click()
466 >>> browser2.url
467- 'http://launchpad.test/%7Emyemail/+member/karl/+index'
468+ 'http://launchpad.test/~myemail/+member/karl/+index'
469 >>> for tag in find_tags_by_class(browser2.contents, 'error message'):
470 ... print tag.renderContents()
471 The membership request for Karl Tilbury has already been processed.
472diff --git a/lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt b/lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt
473index d271730..dfb017d 100644
474--- a/lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt
475+++ b/lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt
476@@ -71,7 +71,7 @@ successful voucher redemption.
477 ... 'LPCBS12-f78df324-0cc2-11dd-8b6b-000000000004']
478 >>> browser.getControl('Redeem').click()
479 >>> print browser.url
480- http://launchpad.test/%7Ecprov/+vouchers
481+ http://launchpad.test/~cprov/+vouchers
482 >>> print_feedback_messages(browser.contents)
483 Voucher redeemed successfully
484
485diff --git a/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt b/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt
486index dcadf49..441d5e9 100644
487--- a/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt
488+++ b/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt
489@@ -52,14 +52,11 @@ minute time interval (set in lp.services.webapp.login and enforced
490 with an assert in lp.services.webapp.session) is intended to be fudge
491 time for browsers with bad system clocks.
492
493- >>> from six.moves.urllib.error import HTTPError
494- >>> browser.mech_browser.set_handle_redirect(False)
495- >>> try:
496- ... browser.getControl('Log Out').click()
497- ... except HTTPError as redirection:
498- ... print(redirection)
499- ... print(redirection.hdrs['Location'])
500- HTTP Error 303: See Other
501+ >>> browser.followRedirects = False
502+ >>> browser.getControl('Log Out').click()
503+ >>> print(browser.headers['Status'])
504+ 303 See Other
505+ >>> print(browser.headers['Location'])
506 https://bazaar.launchpad.test/+logout?next_to=...
507
508 After ensuring the browser has not left the launchpad.test domain, the
509diff --git a/lib/lp/services/webapp/tests/test_login.py b/lib/lp/services/webapp/tests/test_login.py
510index 9aaae8f..dd485b7 100644
511--- a/lib/lp/services/webapp/tests/test_login.py
512+++ b/lib/lp/services/webapp/tests/test_login.py
513@@ -656,9 +656,6 @@ class TestOpenIDCallbackRedirects(TestCaseWithFactory):
514 view.request.response.getHeader('Location'), self.APPLICATION_URL)
515
516
517-urls_redirected_to = []
518-
519-
520 def fill_login_form_and_submit(browser, email_address):
521 assert browser.getControl(name='field.email') is not None, (
522 "We don't seem to be looking at a login form.")
523@@ -671,7 +668,7 @@ class TestOpenIDReplayAttack(TestCaseWithFactory):
524
525 def test_replay_attacks_do_not_succeed(self):
526 browser = Browser()
527- browser.mech_browser.set_handle_redirect(False)
528+ browser.followRedirects = False
529 browser.open('%s/+login' % self.layer.appserver_root_url())
530 # On a JS-enabled browser this page would've been auto-submitted
531 # (thanks to the onload handler), but here we have to do it manually.
532@@ -679,24 +676,22 @@ class TestOpenIDReplayAttack(TestCaseWithFactory):
533 browser.getControl('Continue').click()
534
535 self.assertEqual('Login', browser.title)
536- redirection = self.assertRaises(
537- HTTPError,
538- fill_login_form_and_submit, browser, 'test@canonical.com')
539- self.assertEqual(httplib.FOUND, redirection.code)
540- callback_url = redirection.hdrs['Location']
541+ fill_login_form_and_submit(browser, 'test@canonical.com')
542+ self.assertEqual(
543+ httplib.FOUND, int(browser.headers['Status'].split(' ', 1)[0]))
544+ callback_url = browser.headers['Location']
545 self.assertIn('+openid-callback', callback_url)
546- callback_redirection = self.assertRaises(
547- HTTPError, browser.open, callback_url)
548+ browser.open(callback_url)
549 self.assertEqual(
550- httplib.TEMPORARY_REDIRECT, callback_redirection.code)
551- browser.open(callback_redirection.hdrs['Location'])
552+ httplib.TEMPORARY_REDIRECT,
553+ int(browser.headers['Status'].split(' ', 1)[0]))
554+ browser.open(browser.headers['Location'])
555 login_status = extract_text(
556 find_tag_by_id(browser.contents, 'logincontrol'))
557 self.assertIn('Sample Person (name12)', login_status)
558
559- # Now we look up (in urls_redirected_to) the +openid-callback URL that
560- # was used to complete the authentication and open it on a different
561- # browser with a fresh set of cookies.
562+ # Now we open the +openid-callback URL that was used to complete the
563+ # authentication on a different browser with a fresh set of cookies.
564 replay_browser = Browser()
565 replay_browser.open(callback_url)
566 login_status = extract_text(
567diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
568index 339ab44..165fd1f 100644
569--- a/lib/lp/snappy/browser/tests/test_snap.py
570+++ b/lib/lp/snappy/browser/tests/test_snap.py
571@@ -13,14 +13,12 @@ from datetime import (
572 )
573 import json
574 import re
575-from urllib2 import HTTPError
576 from urlparse import (
577 parse_qs,
578 urlsplit,
579 )
580
581 from fixtures import FakeLogger
582-from mechanize import LinkNotFoundError
583 from pymacaroons import Macaroon
584 import pytz
585 import responses
586@@ -40,6 +38,7 @@ from zope.component import getUtility
587 from zope.publisher.interfaces import NotFound
588 from zope.security.interfaces import Unauthorized
589 from zope.security.proxy import removeSecurityProxy
590+from zope.testbrowser.browser import LinkNotFoundError
591
592 from lp.app.enums import InformationType
593 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
594@@ -478,8 +477,7 @@ class TestSnapAddView(BaseTestSnapView):
595 responses.add(
596 "POST", "http://sca.example/dev/api/acl/",
597 json={"macaroon": root_macaroon_raw})
598- redirection = self.assertRaises(
599- HTTPError, browser.getControl("Create snap package").click)
600+ browser.getControl("Create snap package").click()
601 login_person(self.person)
602 snap = getUtility(ISnapSet).getByName(self.person, "snap-name")
603 self.assertThat(snap, MatchesStructure.byEquality(
604@@ -499,8 +497,8 @@ class TestSnapAddView(BaseTestSnapView):
605 "permissions": ["package_upload"],
606 }
607 self.assertEqual(expected_body, json.loads(call.request.body))
608- self.assertEqual(303, redirection.code)
609- parsed_location = urlsplit(redirection.hdrs["Location"])
610+ self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
611+ parsed_location = urlsplit(browser.headers["Location"])
612 self.assertEqual(
613 urlsplit(
614 canonical_url(snap, rootsite="code") +
615@@ -996,7 +994,7 @@ class TestSnapEditView(BaseTestSnapView):
616 # enables it, they can't enable the restricted processor.
617 for control in processors.controls:
618 if control.optionValue == "armhf":
619- control.mech_item.disabled = False
620+ del control._control.attrs["disabled"]
621 processors.value = ["386", "amd64", "armhf"]
622 self.assertRaises(
623 CannotModifySnapProcessor,
624@@ -1023,7 +1021,8 @@ class TestSnapEditView(BaseTestSnapView):
625 browser = self.getUserBrowser(
626 canonical_url(snap) + "/+edit", user=snap.owner)
627 processors = browser.getControl(name="field.processors")
628- self.assertContentEqual(["386", "amd64"], processors.value)
629+ # armhf is checked but disabled.
630+ self.assertContentEqual(["386", "amd64", "armhf"], processors.value)
631 self.assertProcessorControls(
632 processors, ["386", "amd64", "hppa"], ["armhf"])
633 processors.value = ["386"]
634@@ -1097,8 +1096,7 @@ class TestSnapEditView(BaseTestSnapView):
635 responses.add(
636 "POST", "http://sca.example/dev/api/acl/",
637 json={"macaroon": root_macaroon_raw})
638- redirection = self.assertRaises(
639- HTTPError, browser.getControl("Update snap package").click)
640+ browser.getControl("Update snap package").click()
641 login_person(self.person)
642 self.assertThat(snap, MatchesStructure.byEquality(
643 store_name="two", store_secrets={"root": root_macaroon_raw},
644@@ -1111,8 +1109,8 @@ class TestSnapEditView(BaseTestSnapView):
645 "permissions": ["package_upload"],
646 }
647 self.assertEqual(expected_body, json.loads(call.request.body))
648- self.assertEqual(303, redirection.code)
649- parsed_location = urlsplit(redirection.hdrs["Location"])
650+ self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
651+ parsed_location = urlsplit(browser.headers["Location"])
652 self.assertEqual(
653 urlsplit(canonical_url(snap) + "/+authorize/+login")[:3],
654 parsed_location[:3])
655@@ -1163,8 +1161,7 @@ class TestSnapAuthorizeView(BaseTestSnapView):
656 json={"macaroon": root_macaroon_raw})
657 browser = self.getNonRedirectingBrowser(
658 url=snap_url + "/+authorize", user=self.snap.owner)
659- redirection = self.assertRaises(
660- HTTPError, browser.getControl("Begin authorization").click)
661+ browser.getControl("Begin authorization").click()
662 [call] = responses.calls
663 self.assertThat(call.request, MatchesStructure.byEquality(
664 url="http://sca.example/dev/api/acl/", method="POST"))
665@@ -1179,12 +1176,12 @@ class TestSnapAuthorizeView(BaseTestSnapView):
666 self.assertEqual(expected_body, json.loads(call.request.body))
667 self.assertEqual(
668 {"root": root_macaroon_raw}, self.snap.store_secrets)
669- self.assertEqual(303, redirection.code)
670+ self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
671 self.assertEqual(
672 snap_url + "/+authorize/+login?macaroon_caveat_id=dummy&"
673 "discharge_macaroon_action=field.actions.complete&"
674 "discharge_macaroon_field=field.discharge_macaroon",
675- redirection.hdrs["Location"])
676+ browser.headers["Location"])
677
678 def test_complete_authorization_missing_discharge_macaroon(self):
679 # If the form does not include a discharge macaroon, the "complete"
680diff --git a/lib/lp/snappy/browser/tests/test_snapbuild.py b/lib/lp/snappy/browser/tests/test_snapbuild.py
681index 9676891..3b34639 100644
682--- a/lib/lp/snappy/browser/tests/test_snapbuild.py
683+++ b/lib/lp/snappy/browser/tests/test_snapbuild.py
684@@ -10,7 +10,6 @@ __metaclass__ = type
685 import re
686
687 from fixtures import FakeLogger
688-from mechanize import LinkNotFoundError
689 from pymacaroons import Macaroon
690 import soupmatchers
691 from storm.locals import Store
692@@ -22,6 +21,7 @@ import transaction
693 from zope.component import getUtility
694 from zope.security.interfaces import Unauthorized
695 from zope.security.proxy import removeSecurityProxy
696+from zope.testbrowser.browser import LinkNotFoundError
697
698 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
699 from lp.buildmaster.enums import BuildStatus
700@@ -34,7 +34,6 @@ from lp.testing import (
701 ANONYMOUS,
702 BrowserTestCase,
703 login,
704- logout,
705 person_logged_in,
706 TestCaseWithFactory,
707 )
708@@ -425,24 +424,19 @@ class TestSnapBuildOperations(BrowserTestCase):
709
710 def test_builder_index_public(self):
711 build = self.makeBuildingSnap()
712- builder_url = canonical_url(build.builder)
713- logout()
714- browser = self.getNonRedirectingBrowser(
715- url=builder_url, user=ANONYMOUS)
716+ browser = self.getViewBrowser(build.builder, no_login=True)
717 self.assertIn("tail of the log", browser.contents)
718
719 def test_builder_index_private(self):
720 archive = self.factory.makeArchive(private=True)
721 with admin_logged_in():
722 build = self.makeBuildingSnap(archive=archive)
723- builder_url = canonical_url(build.builder)
724- logout()
725+ builder = removeSecurityProxy(build).builder
726
727 # An unrelated user can't see the logtail of a private build.
728- browser = self.getNonRedirectingBrowser(url=builder_url)
729+ browser = self.getViewBrowser(builder)
730 self.assertNotIn("tail of the log", browser.contents)
731
732 # But someone who can see the archive can.
733- browser = self.getNonRedirectingBrowser(
734- url=builder_url, user=archive.owner)
735+ browser = self.getViewBrowser(builder, user=archive.owner)
736 self.assertIn("tail of the log", browser.contents)
737diff --git a/lib/lp/soyuz/browser/tests/test_archive.py b/lib/lp/soyuz/browser/tests/test_archive.py
738index 8ec40ee..058f63c 100644
739--- a/lib/lp/soyuz/browser/tests/test_archive.py
740+++ b/lib/lp/soyuz/browser/tests/test_archive.py
741@@ -124,7 +124,7 @@ class TestArchiveEditView(TestCaseWithFactory):
742 # enables it, they can't enable the restricted processor.
743 for control in processors.controls:
744 if control.optionValue == "armhf":
745- control.mech_item.disabled = False
746+ del control._control.attrs["disabled"]
747 processors.value = ["386", "amd64", "armhf"]
748 self.assertRaises(
749 CannotModifyArchiveProcessor, browser.getControl("Save").click)
750@@ -147,7 +147,8 @@ class TestArchiveEditView(TestCaseWithFactory):
751 browser = self.getUserBrowser(
752 canonical_url(ppa) + "/+edit", user=ppa.owner)
753 processors = browser.getControl(name="field.processors")
754- self.assertContentEqual(["386", "amd64"], processors.value)
755+ # armhf is checked but disabled.
756+ self.assertContentEqual(["386", "amd64", "armhf"], processors.value)
757 self.assertProcessorControls(
758 processors, ["386", "amd64", "hppa"], ["armhf"])
759 processors.value = ["386"]
760diff --git a/lib/lp/soyuz/browser/tests/test_livefs.py b/lib/lp/soyuz/browser/tests/test_livefs.py
761index 9b68371..86f3d86 100644
762--- a/lib/lp/soyuz/browser/tests/test_livefs.py
763+++ b/lib/lp/soyuz/browser/tests/test_livefs.py
764@@ -14,11 +14,11 @@ from datetime import (
765 import json
766
767 from fixtures import FakeLogger
768-from mechanize import LinkNotFoundError
769 import pytz
770 from zope.component import getUtility
771 from zope.publisher.interfaces import NotFound
772 from zope.security.interfaces import Unauthorized
773+from zope.testbrowser.browser import LinkNotFoundError
774
775 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
776 from lp.buildmaster.enums import BuildStatus
777diff --git a/lib/lp/soyuz/browser/tests/test_livefsbuild.py b/lib/lp/soyuz/browser/tests/test_livefsbuild.py
778index 348d176..b753054 100644
779--- a/lib/lp/soyuz/browser/tests/test_livefsbuild.py
780+++ b/lib/lp/soyuz/browser/tests/test_livefsbuild.py
781@@ -8,7 +8,6 @@ from __future__ import absolute_import, print_function, unicode_literals
782 __metaclass__ = type
783
784 from fixtures import FakeLogger
785-from mechanize import LinkNotFoundError
786 import soupmatchers
787 from storm.locals import Store
788 from testtools.matchers import StartsWith
789@@ -16,6 +15,7 @@ import transaction
790 from zope.component import getUtility
791 from zope.security.interfaces import Unauthorized
792 from zope.security.proxy import removeSecurityProxy
793+from zope.testbrowser.browser import LinkNotFoundError
794
795 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
796 from lp.buildmaster.enums import BuildStatus
797@@ -27,7 +27,6 @@ from lp.testing import (
798 ANONYMOUS,
799 BrowserTestCase,
800 login,
801- logout,
802 person_logged_in,
803 TestCaseWithFactory,
804 )
805@@ -234,24 +233,19 @@ class TestLiveFSBuildOperations(BrowserTestCase):
806
807 def test_builder_index_public(self):
808 build = self.makeBuildingLiveFS()
809- builder_url = canonical_url(build.builder)
810- logout()
811- browser = self.getNonRedirectingBrowser(
812- url=builder_url, user=ANONYMOUS)
813+ browser = self.getViewBrowser(build.builder, no_login=True)
814 self.assertIn("tail of the log", browser.contents)
815
816 def test_builder_index_private(self):
817 archive = self.factory.makeArchive(private=True)
818 with admin_logged_in():
819 build = self.makeBuildingLiveFS(archive=archive)
820- builder_url = canonical_url(build.builder)
821- logout()
822+ builder = removeSecurityProxy(build).builder
823
824 # An unrelated user can't see the logtail of a private build.
825- browser = self.getNonRedirectingBrowser(url=builder_url)
826+ browser = self.getViewBrowser(builder)
827 self.assertNotIn("tail of the log", browser.contents)
828
829 # But someone who can see the archive can.
830- browser = self.getNonRedirectingBrowser(
831- url=builder_url, user=archive.owner)
832+ browser = self.getViewBrowser(builder, user=archive.owner)
833 self.assertIn("tail of the log", browser.contents)
834diff --git a/lib/lp/soyuz/stories/ppa/xx-delete-packages.txt b/lib/lp/soyuz/stories/ppa/xx-delete-packages.txt
835index c6e795b..7f0f2b7 100644
836--- a/lib/lp/soyuz/stories/ppa/xx-delete-packages.txt
837+++ b/lib/lp/soyuz/stories/ppa/xx-delete-packages.txt
838@@ -166,17 +166,19 @@ An nonexistent source:
839
840 >>> admin_browser.getControl(
841 ... name='field.selected_sources').value = ['100']
842- Traceback (most recent call last):
843- ...
844- ItemNotFoundError: insufficient items with name u'100'
845+ >>> admin_browser.getControl('Request Deletion').click()
846+ >>> print_feedback_messages(admin_browser.contents)
847+ There is 1 error.
848+ No sources selected.
849
850 An invalid value.
851
852 >>> admin_browser.getControl(
853 ... name='field.selected_sources').value = ['blah']
854- Traceback (most recent call last):
855- ...
856- ItemNotFoundError: insufficient items with name u'blah'
857+ >>> admin_browser.getControl('Request Deletion').click()
858+ >>> print_feedback_messages(admin_browser.contents)
859+ There is 1 error.
860+ No sources selected.
861
862 The deleted record is now presented accordingly in the +index page. We
863 will use another browser to inspect the results of our deletions.
864diff --git a/lib/lp/soyuz/stories/ppa/xx-ppa-files.txt b/lib/lp/soyuz/stories/ppa/xx-ppa-files.txt
865index d7a180a..2b9cde6 100644
866--- a/lib/lp/soyuz/stories/ppa/xx-ppa-files.txt
867+++ b/lib/lp/soyuz/stories/ppa/xx-ppa-files.txt
868@@ -124,7 +124,7 @@ Links to files accessible via +files/ proxy in the Build page.
869
870 Create a function to check the expected links.
871
872- >>> from mechanize import LinkNotFoundError
873+ >>> from zope.testbrowser.browser import LinkNotFoundError
874 >>> def check_urls(browser, links, base_url):
875 ... for link, libraryfile, source_name, source_version in links:
876 ... try:
877diff --git a/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt b/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt
878index 6a6ceb1..da2791d 100644
879--- a/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt
880+++ b/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt
881@@ -69,7 +69,6 @@ For DistroArchSeries, same as Distribution:
882
883 For Builder, same as Distribution:
884
885- >>> anon_browser.mech_browser.set_handle_equiv(False)
886 >>> anon_browser.open("http://launchpad.test/builders/bob")
887 >>> anon_browser.getLink("View full history").click()
888 >>> print(anon_browser.title)
889@@ -270,7 +269,7 @@ to the form:
890 >>> anon_browser.getControl(name="build_state").value = ['foo']
891 Traceback (most recent call last):
892 ...
893- ItemNotFoundError: insufficient items with name u'foo'
894+ ValueError: Option u'foo' not found ...
895
896 However even if anonymous user builds an URL with a incorrect value,
897 code is prepared to raise the correct exception:
898diff --git a/lib/lp/soyuz/stories/soyuz/xx-private-builds.txt b/lib/lp/soyuz/stories/soyuz/xx-private-builds.txt
899index 9fbe07f..44c2730 100644
900--- a/lib/lp/soyuz/stories/soyuz/xx-private-builds.txt
901+++ b/lib/lp/soyuz/stories/soyuz/xx-private-builds.txt
902@@ -60,7 +60,6 @@ So now we can go to frog's builder page and see what the page shows us.
903 The status shown to an anonymous user hides the private source it is
904 building.
905
906- >>> anon_browser.mech_browser.set_handle_equiv(False)
907 >>> anon_browser.open("http://launchpad.test/+builds/frog")
908 >>> print(extract_text(find_main_content(anon_browser.contents)))
909 The frog builder...
910@@ -70,7 +69,6 @@ building.
911
912 Launchpad Administrators are allowed to see the build:
913
914- >>> admin_browser.mech_browser.set_handle_equiv(False)
915 >>> admin_browser.open("http://launchpad.test/+builds/frog")
916 >>> print(extract_text(find_main_content(admin_browser.contents)))
917 The frog builder...
918@@ -82,7 +80,6 @@ Launchpad Administrators are allowed to see the build:
919 Buildd Administrators are not allowed to see the build in the portlet:
920
921 >>> name12_browser = setupBrowser(auth="Basic test@canonical.com:test")
922- >>> name12_browser.mech_browser.set_handle_equiv(False)
923 >>> name12_browser.open("http://launchpad.test/+builds/frog")
924 >>> print(extract_text(find_main_content(name12_browser.contents)))
925 The frog builder...
926@@ -94,7 +91,6 @@ cprov is also allowed to see his own build:
927
928 >>> cprov_browser = setupBrowser(
929 ... auth="Basic celso.providelo@canonical.com:test")
930- >>> cprov_browser.mech_browser.set_handle_equiv(False)
931 >>> cprov_browser.open("http://launchpad.test/+builds/frog")
932 >>> print(extract_text(find_main_content(cprov_browser.contents)))
933 The frog builder...
934@@ -304,7 +300,6 @@ Now the anonymous user can see the build:
935
936 Any other logged-in user will also see the build:
937
938- >>> browser.mech_browser.set_handle_equiv(False)
939 >>> browser.open("http://launchpad.test/+builds")
940 >>> print(extract_text(find_main_content(browser.contents)))
941 The Launchpad build farm
942diff --git a/lib/lp/testing/__init__.py b/lib/lp/testing/__init__.py
943index 6a0d9b2..0c543e8 100644
944--- a/lib/lp/testing/__init__.py
945+++ b/lib/lp/testing/__init__.py
946@@ -867,8 +867,7 @@ class TestCaseWithFactory(TestCase):
947 browser = setupBrowser()
948 else:
949 browser = self.getUserBrowser(user=user)
950- browser.mech_browser.set_handle_redirect(False)
951- browser.mech_browser.set_handle_equiv(False)
952+ browser.followRedirects = False
953 if url is not None:
954 browser.open(url)
955 return browser
956diff --git a/lib/lp/testing/doc/pagetest-helpers.txt b/lib/lp/testing/doc/pagetest-helpers.txt
957index 0676eb0..25684c4 100644
958--- a/lib/lp/testing/doc/pagetest-helpers.txt
959+++ b/lib/lp/testing/doc/pagetest-helpers.txt
960@@ -27,7 +27,7 @@ one should be use for all anonymous browsing tests.
961 # Shortcut to fetch the Authorization header from the testbrowser
962
963 >>> def getAuthorizationHeader(browser):
964- ... return dict(browser.mech_browser.addheaders).get('Authorization','')
965+ ... return browser._req_headers.get('Authorization', '')
966
967 >>> anon_browser = test.globs['anon_browser']
968 >>> print(getAuthorizationHeader(anon_browser))
969diff --git a/lib/lp/testing/layers.py b/lib/lp/testing/layers.py
970index b90c853..59617f5 100644
971--- a/lib/lp/testing/layers.py
972+++ b/lib/lp/testing/layers.py
973@@ -79,7 +79,10 @@ from fixtures import (
974 MonkeyPatch,
975 )
976 import psycopg2
977-from six.moves.urllib.parse import quote
978+from six.moves.urllib.parse import (
979+ quote,
980+ urlparse,
981+ )
982 from storm.zope.interfaces import IZStorm
983 import transaction
984 from webob.request import environ_from_url as orig_environ_from_url
985@@ -101,6 +104,7 @@ from zope.security.management import (
986 getSecurityPolicy,
987 )
988 from zope.server.logger.pythonlogger import PythonLogger
989+from zope.testbrowser.browser import HostNotAllowed
990 import zope.testbrowser.wsgi
991 from zope.testbrowser.wsgi import AuthorizationMiddleware
992
993@@ -1153,17 +1157,16 @@ class FunctionalLayer(BaseLayer):
994 transaction.begin()
995
996 # Allow the WSGI test browser to talk to our various test hosts.
997- def assert_allowed_host(self):
998- host = self.host
999- if ':' in host:
1000- host = host.split(':')[0]
1001+ def _assertAllowed(self, url):
1002+ parsed = urlparse(url)
1003+ host = parsed.netloc.partition(':')[0]
1004 if host == 'localhost' or host.endswith('.test'):
1005 return
1006- self._allowed = False
1007+ raise HostNotAllowed(url)
1008
1009 FunctionalLayer._testbrowser_allowed = MonkeyPatch(
1010- 'zope.testbrowser.wsgi.WSGIConnection.assert_allowed_host',
1011- assert_allowed_host)
1012+ 'zope.testbrowser.browser.TestbrowserApp._assertAllowed',
1013+ _assertAllowed)
1014 FunctionalLayer._testbrowser_allowed.setUp()
1015 FunctionalLayer.browser_layer.testSetUp()
1016
1017diff --git a/lib/lp/testing/pages.py b/lib/lp/testing/pages.py
1018index cff8368..705b02a 100644
1019--- a/lib/lp/testing/pages.py
1020+++ b/lib/lp/testing/pages.py
1021@@ -28,6 +28,7 @@ from BeautifulSoup import (
1022 Tag,
1023 )
1024 from bs4.element import (
1025+ CData as CData4,
1026 Comment as Comment4,
1027 Declaration as Declaration4,
1028 Doctype as Doctype4,
1029@@ -44,8 +45,12 @@ from contrib.oauth import (
1030 )
1031 from lazr.restful.testing.webservice import WebServiceCaller
1032 import six
1033+from soupsieve import escape as css_escape
1034 import transaction
1035-from webtest import TestRequest
1036+from webtest import (
1037+ forms,
1038+ TestRequest,
1039+ )
1040 from zope.app.wsgi.testlayer import (
1041 FakeResponse as _FakeResponse,
1042 NotInBrowserLayer,
1043@@ -54,8 +59,15 @@ from zope.component import getUtility
1044 from zope.security.management import setSecurityPolicy
1045 from zope.security.proxy import removeSecurityProxy
1046 from zope.session.interfaces import ISession
1047+from zope.testbrowser.browser import (
1048+ BrowserStateError,
1049+ isMatching,
1050+ Link as _Link,
1051+ LinkNotFoundError,
1052+ normalizeWhitespace,
1053+ )
1054 from zope.testbrowser.wsgi import (
1055- Browser,
1056+ Browser as _Browser,
1057 Layer as TestBrowserWSGILayer,
1058 )
1059
1060@@ -74,7 +86,10 @@ from lp.services.oauth.interfaces import (
1061 from lp.services.webapp import canonical_url
1062 from lp.services.webapp.authorization import LaunchpadPermissiveSecurityPolicy
1063 from lp.services.webapp.interfaces import OAuthPermission
1064-from lp.services.webapp.servers import LaunchpadTestRequest
1065+from lp.services.webapp.servers import (
1066+ LaunchpadTestRequest,
1067+ wsgi_native_string,
1068+ )
1069 from lp.services.webapp.url import urlsplit
1070 from lp.testing import (
1071 ANONYMOUS,
1072@@ -100,6 +115,11 @@ SAMPLEDATA_ACCESS_SECRETS = {
1073 }
1074
1075
1076+# Teach WebTest about <input type="search" />.
1077+# https://github.com/Pylons/webtest/pull/219
1078+forms.Field.classes['search'] = forms.Text
1079+
1080+
1081 class FakeResponse(_FakeResponse):
1082 """A fake response for use in tests.
1083
1084@@ -197,8 +217,7 @@ class LaunchpadWebServiceCaller(WebServiceCaller):
1085 self.access_token)
1086 oauth_headers = request.to_header(OAUTH_REALM)
1087 full_headers.update({
1088- six.ensure_str(key, encoding='ISO-8859-1'):
1089- six.ensure_str(value, encoding='ISO-8859-1')
1090+ wsgi_native_string(key): wsgi_native_string(value)
1091 for key, value in oauth_headers.items()})
1092 if not self.handle_errors:
1093 full_headers['X_Zope_handle_errors'] = 'False'
1094@@ -693,6 +712,67 @@ def print_errors(contents):
1095 print(error)
1096
1097
1098+class Link(_Link):
1099+ """`zope.testbrowser.browser.Link`, but with image alt text handling."""
1100+
1101+ @property
1102+ def text(self):
1103+ txt = normalizeWhitespace(self.browser._getText(self._link))
1104+ return self.browser.toStr(txt)
1105+
1106+
1107+class Browser(_Browser):
1108+ """A modified Browser with behaviour more suitable for pagetests."""
1109+
1110+ def reload(self):
1111+ """Make a new request rather than reusing an existing one."""
1112+ if self.url is None:
1113+ raise BrowserStateError("no URL has yet been .open()ed")
1114+ self.open(self.url, referrer=self._req_referrer)
1115+
1116+ def addHeader(self, key, value):
1117+ """Make sure headers are native strings."""
1118+ super(Browser, self).addHeader(
1119+ wsgi_native_string(key), wsgi_native_string(value))
1120+
1121+ def _getText(self, element):
1122+ def get_strings(elem):
1123+ for descendant in elem.descendants:
1124+ if isinstance(descendant, (NavigableString4, CData4)):
1125+ yield descendant
1126+ elif isinstance(descendant, Tag4) and descendant.name == 'img':
1127+ yield u'%s[%s]' % (
1128+ descendant.get('alt', u''), descendant.name.upper())
1129+
1130+ return u''.join(list(get_strings(element)))
1131+
1132+ def getLink(self, text=None, url=None, id=None, index=0):
1133+ """Search for both text nodes and image alt attributes."""
1134+ # XXX cjwatson 2019-11-09: This should be merged back into
1135+ # `zope.testbrowser.browser.Browser.getLink`.
1136+ qa = 'a' if id is None else 'a#%s' % css_escape(id)
1137+ qarea = 'area' if id is None else 'area#%s' % css_escape(id)
1138+ html = self._html
1139+ links = html.select(qa)
1140+ links.extend(html.select(qarea))
1141+
1142+ matching = []
1143+ for elem in links:
1144+ matches = (isMatching(self._getText(elem), text) and
1145+ isMatching(elem.get('href', ''), url))
1146+
1147+ if matches:
1148+ matching.append(elem)
1149+
1150+ if index >= len(matching):
1151+ raise LinkNotFoundError()
1152+ elem = matching[index]
1153+
1154+ baseurl = self._getBaseUrl()
1155+
1156+ return Link(elem, self, baseurl)
1157+
1158+
1159 def setupBrowser(auth=None):
1160 """Create a testbrowser object for use in pagetests.
1161
1162diff --git a/lib/lp/translations/stories/buildfarm/xx-build-summary.txt b/lib/lp/translations/stories/buildfarm/xx-build-summary.txt
1163index e114e3c..230b756 100644
1164--- a/lib/lp/translations/stories/buildfarm/xx-build-summary.txt
1165+++ b/lib/lp/translations/stories/buildfarm/xx-build-summary.txt
1166@@ -70,7 +70,6 @@ Show summary
1167 The job's summary shows that what type of job this is. It also links
1168 to the branch.
1169
1170- >>> user_browser.mech_browser.set_handle_equiv(False)
1171 >>> user_browser.open(builder_page)
1172 >>> print(extract_text(find_build_summary(user_browser)))
1173 Working on TranslationTemplatesBuild for branch ...
1174diff --git a/lib/lp/translations/stories/standalone/xx-pofile-translate-alternative-language.txt b/lib/lp/translations/stories/standalone/xx-pofile-translate-alternative-language.txt
1175index e4991da..fc97599 100644
1176--- a/lib/lp/translations/stories/standalone/xx-pofile-translate-alternative-language.txt
1177+++ b/lib/lp/translations/stories/standalone/xx-pofile-translate-alternative-language.txt
1178@@ -24,16 +24,18 @@ in the form's main language.
1179 ... """
1180 ... if not browser.url.endswith('/+translate'):
1181 ... raise AssertionError("Not a +translate page: " + browser.url)
1182+ ... alternative_language = browser.getControl(
1183+ ... name='field.alternative_language')
1184 ... try:
1185- ... browser.getControl('English (en)', index=0).selected
1186+ ... alternative_language.getControl(
1187+ ... browser.toStr('English (en)')).selected
1188 ... raise AssertionError(
1189 ... "Looking up English among alternative languages "
1190 ... "should have failed, but didn't.")
1191 ... except LookupError:
1192 ... pass
1193 ...
1194- ... return browser.getControl(
1195- ... name='field.alternative_language')
1196+ ... return alternative_language
1197
1198 An anonymous user is offered all available languages except English for
1199 alternative suggestions. We do not offer suggestions from standard English
1200@@ -79,7 +81,8 @@ Spanish as an alternative language.
1201
1202 >>> import re
1203 >>> browser.open(re.sub('/es/', '/ca/', translate_page))
1204- >>> browser.getControl('Spanish (es)').selected = True
1205+ >>> get_alternative_languages_widget(browser).getControl(
1206+ ... 'Spanish (es)').selected = True
1207 >>> browser.getControl('Change').click()
1208
1209 The Spanish translations now show up as suggestions. For example, where
1210@@ -131,7 +134,8 @@ and other alternative languages does not exist, of course, if no preferred
1211 languages are defined. Suggestions just work for anonymous users.
1212
1213 >>> anon_browser.open(re.sub('/es/', '/ca/', translate_page))
1214- >>> anon_browser.getControl('Spanish (es)').selected = True
1215+ >>> get_alternative_languages_widget(anon_browser).getControl(
1216+ ... anon_browser.toStr('Spanish (es)')).selected = True
1217 >>> anon_browser.getControl('Change').click()
1218
1219 >>> print(extract_text(find_main_content(
1220@@ -161,8 +165,10 @@ show only the strings they are interested in.
1221 Carlos sets the filter to display only the untranslated strings.
1222
1223 >>> browser.open(translate_page)
1224- >>> browser.getControl('Catalan (ca)').selected = True
1225- >>> browser.getControl('untranslated').selected = True
1226+ >>> get_alternative_languages_widget(browser).getControl(
1227+ ... 'Catalan (ca)').selected = True
1228+ >>> browser.getControl('Translating').getControl(
1229+ ... 'untranslated').selected = True
1230 >>> browser.getControl('Change').click()
1231 >>> print(extract_url_parameter(
1232 ... browser.url, 'field.alternative_language'))
1233diff --git a/lib/lp/translations/stories/standalone/xx-pofile-translate-lang-direction.txt b/lib/lp/translations/stories/standalone/xx-pofile-translate-lang-direction.txt
1234index dc1c0e2..cf3e9e4 100644
1235--- a/lib/lp/translations/stories/standalone/xx-pofile-translate-lang-direction.txt
1236+++ b/lib/lp/translations/stories/standalone/xx-pofile-translate-lang-direction.txt
1237@@ -16,11 +16,11 @@ the separator in language codes rather than an underscore.
1238 ... 'http://translations.launchpad.test/ubuntu/hoary/+source/evolution/'
1239 ... '+pots/evolution-2.2/en_AU/+translate')
1240 >>> control = browser.getControl(name="msgset_130_en_AU_translation_0_new")
1241- >>> control.mech_control.attrs.get('dir')
1242- 'ltr'
1243+ >>> print(control._control.attrs.get('dir'))
1244+ ltr
1245 >>> control = browser.getControl(name="msgset_139_en_AU_translation_0_new")
1246- >>> control.mech_control.attrs.get('dir')
1247- 'ltr'
1248+ >>> print(control._control.attrs.get('dir'))
1249+ ltr
1250
1251 When entering Hebrew translations, the form controls are set to right to left:
1252
1253@@ -28,11 +28,11 @@ When entering Hebrew translations, the form controls are set to right to left:
1254 ... 'http://translations.launchpad.test/ubuntu/hoary/+source/evolution/'
1255 ... '+pots/evolution-2.2/he/+translate')
1256 >>> control = browser.getControl(name="msgset_130_he_translation_0_new")
1257- >>> control.mech_control.attrs.get('dir')
1258- 'rtl'
1259+ >>> print(control._control.attrs.get('dir'))
1260+ rtl
1261 >>> control = browser.getControl(name="msgset_139_he_translation_0_new")
1262- >>> control.mech_control.attrs.get('dir')
1263- 'rtl'
1264+ >>> print(control._control.attrs.get('dir'))
1265+ rtl
1266
1267 If we post the form with suggestions, the form controls are still set to rtl:
1268
1269@@ -40,8 +40,8 @@ If we post the form with suggestions, the form controls are still set to rtl:
1270 ... 'http://translations.launchpad.test/ubuntu/hoary/+source/evolution/'
1271 ... '+pots/evolution-2.2/he/+translate?field.alternative_language=es')
1272 >>> control = browser.getControl(name="msgset_130_he_translation_0_new")
1273- >>> control.mech_control.attrs.get('dir')
1274- 'rtl'
1275+ >>> print(control._control.attrs.get('dir'))
1276+ rtl
1277
1278 But suggestion text is tagged with its language code and its own text
1279 direction:
1280diff --git a/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt b/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt
1281index b7cb1c1..e2de6c3 100644
1282--- a/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt
1283+++ b/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt
1284@@ -411,7 +411,8 @@ Person submits Chinese translations using Spanish suggestions.
1285 ... '+source/evolution/+pots/evolution-2.2/zh_CN/+translate')
1286 >>> user_browser.getControl(name='show', index=1).value = ['untranslated']
1287 >>> user_browser.getControl('Change').click()
1288- >>> user_browser.getControl('Spanish (es)').selected = True
1289+ >>> user_browser.getControl(name='field.alternative_language').getControl(
1290+ ... user_browser.toStr('Spanish (es)')).selected = True
1291 >>> user_browser.getControl('Change').click()
1292
1293 >>> user_browser.getControl(
1294diff --git a/lib/lp/translations/stories/standalone/xx-sourcepackage-export.txt b/lib/lp/translations/stories/standalone/xx-sourcepackage-export.txt
1295index b425d6e..04994c8 100644
1296--- a/lib/lp/translations/stories/standalone/xx-sourcepackage-export.txt
1297+++ b/lib/lp/translations/stories/standalone/xx-sourcepackage-export.txt
1298@@ -32,7 +32,7 @@ users who are involved in certain ways, in order to keep load to a
1299 reasonable level.
1300
1301 >>> from zope.security.interfaces import Unauthorized
1302- >>> from mechanize import LinkNotFoundError
1303+ >>> from zope.testbrowser.browser import LinkNotFoundError
1304
1305 >>> def can_download_translations(browser):
1306 ... """Can browser download full package translations?
1307diff --git a/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt b/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt
1308index e058852..ceaba7d 100644
1309--- a/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt
1310+++ b/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt
1311@@ -746,12 +746,6 @@ languages, they will not be able to add or change translations.
1312 Let's see if No Privileges Person can see the translated strings in
1313 Southern Sotho. We expect them to see a readonly form:
1314
1315- >>> delpoints = []
1316- >>> for pos, (key, _) in enumerate(browser.mech_browser.addheaders):
1317- ... if key == 'Authorization':
1318- ... delpoints.append(pos)
1319- >>> for pos in reversed(delpoints):
1320- ... del browser.mech_browser.addheaders[pos]
1321 >>> browser.addHeader(
1322 ... 'Authorization', 'Basic no-priv@canonical.com:test')
1323 >>> browser.open(
1324diff --git a/setup.py b/setup.py
1325index a0518bf..6a72cb8 100644
1326--- a/setup.py
1327+++ b/setup.py
1328@@ -182,7 +182,6 @@ setup(
1329 'lazr.uri',
1330 'lpjsmin',
1331 'Markdown',
1332- 'mechanize',
1333 'meliae',
1334 # Pin version for now to avoid confusion with system site-packages.
1335 'mock==1.0.1',
1336diff --git a/utilities/paste b/utilities/paste
1337index d8be29b..b2df8e0 100755
1338--- a/utilities/paste
1339+++ b/utilities/paste
1340@@ -19,7 +19,7 @@ import urllib
1341 from urlparse import urljoin
1342 import webbrowser
1343
1344-from mechanize import HTTPRobotRulesProcessor
1345+from fixtures import MonkeyPatch
1346 from zope.testbrowser.browser import Browser
1347
1348 # Should we be able to override any of these?
1349@@ -127,17 +127,14 @@ def main():
1350 if lp_cookie is None:
1351 print LP_AUTH_INSTRUCTIONS
1352 return
1353- cookiejar = CookieJar()
1354- cookiejar.set_cookie(lp_cookie)
1355- browser.mech_browser.set_cookiejar(cookiejar)
1356+ browser.testapp.cookiejar.set_cookie(lp_cookie)
1357
1358 # Remove the check for robots.txt, since the one on
1359 # pastebin.ubuntu.com doesn't allow us to open the page. We're not
1360 # really a robot.
1361- browser.mech_browser.handlers = [
1362- handler for handler in browser.mech_browser.handlers
1363- if not isinstance(handler, HTTPRobotRulesProcessor)]
1364- browser.open(urljoin('https://' + paste_host, PASTE_PATH))
1365+ with MonkeyPatch(
1366+ 'six.moves.urllib.robotparser.RobotFileParser.allow_all', True):
1367+ browser.open(urljoin('https://' + paste_host, PASTE_PATH))
1368
1369 if parser.options.private:
1370 # We need to authenticate before pasting.

Subscribers

People subscribed via source and target branches