Commit 1abbcf66 authored by Hanno Schlichting's avatar Hanno Schlichting

Update to zope.testbrowser 5.0 and its WebTest based implementation.

This removes the mechanize dependency.
parent b097a0f8
...@@ -20,12 +20,15 @@ Features Added ...@@ -20,12 +20,15 @@ Features Added
- Updated distributions: - Updated distributions:
- zope.testbrowser = 5.0
- zope.globalrequest = 1.3 - zope.globalrequest = 1.3
- zope.testing = 4.6.0 - zope.testing = 4.6.0
Restructuring Restructuring
+++++++++++++ +++++++++++++
- Update to zope.testbrowser 5.0 and its WebTest based implementation.
- Use `@implementer` and `@adapter` class decorators. - Use `@implementer` and `@adapter` class decorators.
4.0a2 (2016-09-09) 4.0a2 (2016-09-09)
......
...@@ -19,6 +19,7 @@ parts = ...@@ -19,6 +19,7 @@ parts =
requirements requirements
sources-dir = develop sources-dir = develop
auto-checkout = auto-checkout =
ZServer
[test] [test]
......
...@@ -26,7 +26,6 @@ ZODB==5.0.0 ...@@ -26,7 +26,6 @@ ZODB==5.0.0
ZServer==4.0a1 ZServer==4.0a1
five.globalrequest==1.0 five.globalrequest==1.0
funcsigs==1.0.2 funcsigs==1.0.2
mechanize==0.2.5
mock==2.0.0 mock==2.0.0
pbr==1.10.0 pbr==1.10.0
persistent==4.2.1 persistent==4.2.1
...@@ -80,7 +79,7 @@ zope.size==4.1.0 ...@@ -80,7 +79,7 @@ zope.size==4.1.0
zope.structuredtext==4.1.0 zope.structuredtext==4.1.0
zope.tal==4.2.0 zope.tal==4.2.0
zope.tales==4.1.1 zope.tales==4.1.1
zope.testbrowser==4.0.4 zope.testbrowser==5.0.0
zope.testing==4.6.0 zope.testing==4.6.0
zope.testrunner==4.5.1 zope.testrunner==4.5.1
zope.traversing==4.1.0 zope.traversing==4.1.0
......
...@@ -12,148 +12,54 @@ ...@@ -12,148 +12,54 @@
# #
############################################################################## ##############################################################################
"""Support for using zope.testbrowser from Zope2. """Support for using zope.testbrowser from Zope2.
Mostly just copy and paste from zope.testbrowser.testing.
""" """
import io import transaction
import mechanize
from six.moves.urllib.request import HTTPHandler
from zExceptions import status_reasons
from zope.testbrowser import browser from zope.testbrowser import browser
from Testing.ZopeTestCase.zopedoctest import functional from Testing.ZopeTestCase.functional import savestate
from Testing.ZopeTestCase.sandbox import AppZapper
try: from Testing.ZopeTestCase.zopedoctest.functional import auth_header
from http.client import HTTPMessage from ZPublisher.httpexceptions import HTTPExceptionHandler
from urllib.request import AbstractHTTPHandler from ZPublisher.WSGIPublisher import publish_module
except ImportError:
from httplib import HTTPMessage
from urllib2 import AbstractHTTPHandler
class PublisherConnection(object):
def __init__(self, host, timeout=None):
self.caller = functional.http
self.host = host
self.response = None
def set_debuglevel(self, level):
pass
def _quote(self, url):
# the publisher expects to be able to split on whitespace, so we have
# to make sure there is none in the URL
return url.replace(' ', '%20')
def request(self, method, url, body=None, headers=None):
"""Send a request to the publisher.
The response will be stored in ``self.response``.
"""
if body is None:
body = ''
if url == '':
url = '/'
url = self._quote(url)
# Extract the handle_error option header
handle_errors_key = 'X-Zope-Handle-Errors'
handle_errors_header = headers.get(handle_errors_key, True)
if handle_errors_key in headers:
del headers[handle_errors_key]
# Translate string to boolean.
handle_errors = {'False': False}.get(handle_errors_header, True)
# Construct the headers. class WSGITestApp(object):
header_chunks = []
if headers is not None:
for header in headers.items():
header_chunks.append('%s: %s' % header)
headers = '\n'.join(header_chunks) + '\n'
else:
headers = ''
# Construct the full HTTP request string, since that is what the def __init__(self, browser):
# ``HTTPCaller`` wants. self.browser = browser
request_string = (method + ' ' + url + ' HTTP/1.1\n' +
headers + '\n' + body)
self.response = self.caller(request_string, handle_errors)
def getresponse(self): @savestate
"""Return a ``urllib`` compatible response. def __call__(self, environ, start_response):
# This is similar to
# Testing.ZopeTestCase.zopedoctest.functional.http
The goal of ths method is to convert the Zope Publisher's response to # Commit previously done work
a ``urllib`` compatible response, which is also understood by transaction.commit()
mechanize.
"""
real_response = self.response._response
status = real_response.getStatus()
reason = status_reasons[real_response.status]
# Replace HTTP/1.1 200 OK with Status: 200 OK line. # Base64 encode auth header
headers = ['Status: %s %s' % (status, reason)] http_auth = 'HTTP_AUTHORIZATION'
wsgi_headers = self.response._wsgi_headers.getvalue().split('\r\n') if http_auth in environ:
headers += [line for line in wsgi_headers[1:]] environ[http_auth] = auth_header(environ[http_auth])
headers = '\r\n'.join(headers)
content = self.response.getBody()
return PublisherResponse(content, headers, status, reason)
publish = publish_module
if self.browser.handleErrors:
publish = HTTPExceptionHandler(publish)
wsgi_result = publish(environ, start_response)
class PublisherResponse(object): # Sync transaction
"""``mechanize`` compatible response object.""" AppZapper().app()._p_jar.sync()
def __init__(self, content, headers, status, reason): return wsgi_result
self.content = content
self.status = status
self.reason = reason
self.msg = HTTPMessage(io.BytesIO(headers), 0)
self.content_as_file = io.BytesIO(self.content)
def read(self, amt=None):
return self.content_as_file.read(amt)
def close(self):
"""To overcome changes in mechanize and socket in python2.5"""
pass
class PublisherHTTPHandler(HTTPHandler):
"""Special HTTP handler to use the Zope Publisher."""
http_request = AbstractHTTPHandler.do_request_
def http_open(self, req):
"""Open an HTTP connection having a ``urllib`` request."""
# Here we connect to the publisher.
return self.do_open(PublisherConnection, req)
class PublisherMechanizeBrowser(mechanize.Browser):
"""Special ``mechanize`` browser using the Zope Publisher HTTP handler."""
default_schemes = ['http']
default_others = ['_http_error', '_http_request_upgrade',
'_http_default_error']
default_features = ['_redirect', '_cookies', '_referer', '_refresh',
'_equiv', '_basicauth', '_digestauth']
def __init__(self, *args, **kws):
self.handler_classes = mechanize.Browser.handler_classes.copy()
self.handler_classes["http"] = PublisherHTTPHandler
self.default_others = [cls for cls in self.default_others
if cls in mechanize.Browser.handler_classes]
mechanize.Browser.__init__(self, *args, **kws)
class Browser(browser.Browser): class Browser(browser.Browser):
"""A Zope ``testbrowser` Browser that uses the Zope Publisher.""" """A Zope ``testbrowser` Browser that uses the Zope Publisher."""
def __init__(self, url=None): handleErrors = True
mech_browser = PublisherMechanizeBrowser() raiseHttpErrors = True
# override the http handler class
mech_browser.handler_classes["http"] = PublisherHTTPHandler def __init__(self, url=None, wsgi_app=None):
super(Browser, self).__init__(url=url, mech_browser=mech_browser) if wsgi_app is None:
wsgi_app = WSGITestApp(self)
super(Browser, self).__init__(url=url, wsgi_app=wsgi_app)
...@@ -11,12 +11,20 @@ ...@@ -11,12 +11,20 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
"""Unit tests for the testbrowser module. """Tests for the testbrowser module.
""" """
import unittest from AccessControl.Permissions import view
from Testing.ZopeTestCase import FunctionalDocTestSuite from six.moves.urllib.error import HTTPError
from zExceptions import NotFound
from OFS.SimpleItem import Item from OFS.SimpleItem import Item
from Testing.testbrowser import Browser
from Testing.ZopeTestCase import (
FunctionalTestCase,
user_name,
user_password,
)
class CookieStub(Item): class CookieStub(Item):
...@@ -27,57 +35,102 @@ class CookieStub(Item): ...@@ -27,57 +35,102 @@ class CookieStub(Item):
return 'Stub' return 'Stub'
def doctest_cookies(): class ExceptionStub(Item):
""" """This is a stub, raising an exception."""
We want to make sure that our testbrowser correctly understands
cookies. We'll add a stub to ``self.folder`` that sets a cookie.
>>> from Testing.tests.test_testbrowser import CookieStub
>>> self.folder._setObject('stub', CookieStub())
'stub'
This response looks alright:
>>> response = self.publish('/test_folder_1_/stub')
>>> print(str(response)) #doctest: +ELLIPSIS
HTTP/1.1 200 OK
...
Set-Cookie: evil="cookie"
...
Let's try to look at the same folder with testbrowser:
>>> from Testing.testbrowser import Browser
>>> browser = Browser()
>>> browser.open('http://localhost/test_folder_1_/stub')
>>> 'Set-Cookie: evil="cookie"' in str(browser.headers)
True
"""
def __call__(self, REQUEST):
def doctest_camel_case_headers(): raise ValueError('dummy')
"""Make sure that the headers come out in camel case.
Some setup: class TestTestbrowser(FunctionalTestCase):
>>> from Testing.tests.test_testbrowser import CookieStub def test_auth(self):
>>> self.folder._setObject('stub', CookieStub()) # Based on Testing.ZopeTestCase.testFunctional
'stub' basic_auth = '%s:%s' % (user_name, user_password)
self.folder.addDTMLDocument('secret_html', file='secret')
The Zope2 response mungs headers so they come out in camel case we should self.folder.secret_html.manage_permission(view, ['Owner'])
do the same. We will test a few: path = '/' + self.folder.absolute_url(1) + '/secret_html'
>>> from Testing.testbrowser import Browser # Test direct publishing
>>> browser = Browser() response = self.publish(path + '/secret_html')
>>> browser.open('http://localhost/test_folder_1_/stub') self.assertEqual(response.getStatus(), 401)
>>> 'Content-Length: ' in str(browser.headers) response = self.publish(path + '/secret_html', basic_auth)
True self.assertEqual(response.getStatus(), 200)
>>> 'Content-Type: ' in str(browser.headers) self.assertEqual(response.getBody(), 'secret')
True
""" # Test browser
url = 'http://localhost' + path
browser = Browser()
browser.raiseHttpErrors = False
browser.open(url)
self.assertTrue(browser.headers['status'].startswith('401'))
browser.addHeader('Authorization', 'Basic ' + basic_auth)
browser.open(url)
self.assertTrue(browser.headers['status'].startswith('200'))
self.assertEqual(browser.contents, 'secret')
def test_cookies(self):
# We want to make sure that our testbrowser correctly
# understands cookies.
self.folder._setObject('stub', CookieStub())
# Test direct publishing
response = self.publish('/test_folder_1_/stub')
self.assertEqual(response.getCookie('evil')['value'], 'cookie')
browser = Browser()
browser.open('http://localhost/test_folder_1_/stub')
self.assertEqual(browser.cookies.get('evil'), '"cookie"')
def test_handle_errors_true(self):
self.folder._setObject('stub', ExceptionStub())
browser = Browser()
with self.assertRaises(HTTPError):
browser.open('http://localhost/test_folder_1_/stub')
self.assertTrue(browser.headers['status'].startswith('500'))
with self.assertRaises(HTTPError):
browser.open('http://localhost/nothing-is-here')
self.assertTrue(browser.headers['status'].startswith('404'))
def test_handle_errors_false(self):
self.folder._setObject('stub', ExceptionStub())
browser = Browser()
browser.handleErrors = False
with self.assertRaises(ValueError):
browser.open('http://localhost/test_folder_1_/stub')
self.assertTrue(browser.contents is None)
with self.assertRaises(NotFound):
browser.open('http://localhost/nothing-is-here')
self.assertTrue(browser.contents is None)
def test_raise_http_errors_false(self):
self.folder._setObject('stub', ExceptionStub())
browser = Browser()
browser.raiseHttpErrors = False
browser.open('http://localhost/test_folder_1_/stub')
self.assertTrue(browser.headers['status'].startswith('500'))
browser.open('http://localhost/nothing-is-here')
self.assertTrue(browser.headers['status'].startswith('404'))
def test_headers_camel_case(self):
# The Zope2 response mungs headers so they come out
# in camel case. We should do the same.
self.folder._setObject('stub', CookieStub())
browser = Browser()
browser.open('http://localhost/test_folder_1_/stub')
header_text = str(browser.headers)
self.assertTrue('Content-Length: ' in header_text)
self.assertTrue('Content-Type: ' in header_text)
def test_suite(): def test_suite():
return unittest.TestSuite(( from unittest import TestSuite, makeSuite
FunctionalDocTestSuite(), suite = TestSuite()
)) suite.addTest(makeSuite(TestTestbrowser))
return suite
...@@ -12,7 +12,6 @@ DocumentTemplate = 2.13.2 ...@@ -12,7 +12,6 @@ DocumentTemplate = 2.13.2
ExtensionClass = 4.1.2 ExtensionClass = 4.1.2
five.globalrequest = 1.0 five.globalrequest = 1.0
funcsigs = 1.0.2 funcsigs = 1.0.2
mechanize = 0.2.5
mock = 2.0.0 mock = 2.0.0
Missing = 3.1 Missing = 3.1
MultiMapping = 3.0 MultiMapping = 3.0
...@@ -82,7 +81,7 @@ zope.size = 4.1.0 ...@@ -82,7 +81,7 @@ zope.size = 4.1.0
zope.structuredtext = 4.1.0 zope.structuredtext = 4.1.0
zope.tal = 4.2.0 zope.tal = 4.2.0
zope.tales = 4.1.1 zope.tales = 4.1.1
zope.testbrowser = 4.0.4 zope.testbrowser = 5.0.0
zope.testing = 4.6.0 zope.testing = 4.6.0
zope.testrunner = 4.5.1 zope.testrunner = 4.5.1
zope.traversing = 4.1.0 zope.traversing = 4.1.0
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment