[Checkins] SVN: zope.testbrowser/trunk/ Merge webtest3 branch:

Brian Sutherland jinty at web.de
Mon Mar 14 04:52:35 EDT 2011


Log message for revision 120900:
  Merge webtest3 branch:
  
  - Integrate with WebTest. ``zope.testbrowser.wsgi.Browser`` is a
    ``Browser`` implementation that uses ``webtest.TestApp`` to drive a WSGI
    application. This this replaces the wsgi_intercept support added in 3.11.
  
  - Re-write the test application as a pure WSGI application using WebOb. Run the
    existing tests using the WebTest based Browser
  
  - Move zope.app.testing based Browser into ``zope.app.testing`` (leaving
    backwards compatibility imports in-place). Released in ``zope.app.testing``
    3.9.0.
  
  

Changed:
  U   zope.testbrowser/trunk/CHANGES.txt
  U   zope.testbrowser/trunk/buildout.cfg
  U   zope.testbrowser/trunk/setup.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/README.txt
  A   zope.testbrowser/trunk/src/zope/testbrowser/connection.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt
  U   zope.testbrowser/trunk/src/zope/testbrowser/fixed-bugs.txt
  U   zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/ftests/controls.html
  U   zope.testbrowser/trunk/src/zope/testbrowser/ftests/forms.html
  D   zope.testbrowser/trunk/src/zope/testbrowser/ftests/ftesting.zcml
  U   zope.testbrowser/trunk/src/zope/testbrowser/ftests/navigate.html
  U   zope.testbrowser/trunk/src/zope/testbrowser/ftests/textarea.html
  A   zope.testbrowser/trunk/src/zope/testbrowser/ftests/wsgitestapp.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/testing.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/tests/helper.py
  A   zope.testbrowser/trunk/src/zope/testbrowser/tests/test_bbb.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/tests/test_browser.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/tests/test_doctests.py
  U   zope.testbrowser/trunk/src/zope/testbrowser/tests/test_wsgi.py
  D   zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py
  A   zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py

-=-
Modified: zope.testbrowser/trunk/CHANGES.txt
===================================================================
--- zope.testbrowser/trunk/CHANGES.txt	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/CHANGES.txt	2011-03-14 08:52:34 UTC (rev 120900)
@@ -2,12 +2,23 @@
 CHANGES
 =======
 
-3.11.2 (unreleased)
--------------------
+4.0.0 (2011-03-14)
+------------------
 
 - LP #721252: AmbiguityError now shows all matching controls.
 
+- Integrate with WebTest. ``zope.testbrowser.wsgi.Browser`` is a
+  ``Browser`` implementation that uses ``webtest.TestApp`` to drive a WSGI
+  application. This this replaces the wsgi_intercept support added in 3.11.
 
+- Re-write the test application as a pure WSGI application using WebOb. Run the
+  existing tests using the WebTest based Browser
+
+- Move zope.app.testing based Browser into ``zope.app.testing`` (leaving
+  backwards compatibility imports in-place). Released in ``zope.app.testing``
+  3.9.0.
+
+
 3.11.1 (2011-01-24)
 -------------------
 

Modified: zope.testbrowser/trunk/buildout.cfg
===================================================================
--- zope.testbrowser/trunk/buildout.cfg	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/buildout.cfg	2011-03-14 08:52:34 UTC (rev 120900)
@@ -1,12 +1,17 @@
 [buildout]
 develop = .
-parts = test interpreter
+parts = test test_bbb interpreter
 
 [test]
 recipe = zc.recipe.testrunner
 defaults = ['--tests-pattern', '^f?tests$']
-eggs = zope.testbrowser [test, wsgi]
+eggs = zope.testbrowser [test]
 
+[test_bbb]
+recipe = zc.recipe.testrunner
+defaults = ['--tests-pattern', '^f?tests$']
+eggs = zope.testbrowser [test,test_bbb]
+
 [interpreter]
 recipe = zc.recipe.egg
 eggs = zope.testbrowser

Modified: zope.testbrowser/trunk/setup.py
===================================================================
--- zope.testbrowser/trunk/setup.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/setup.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -25,9 +25,13 @@
     + open('CHANGES.txt').read()
     )
 
+tests_require = ['zope.testing',
+                 'zope.pagetemplate',
+                 'WebTest'],
+
 setup(
     name = 'zope.testbrowser',
-    version='3.11.2dev',
+    version='4.0.0dev',
     url = 'http://pypi.python.org/pypi/zope.testbrowser',
     license = 'ZPL 2.1',
     description = 'Programmable browser for functional black-box tests',
@@ -46,7 +50,8 @@
     packages = find_packages('src'),
     package_dir = {'': 'src'},
     namespace_packages = ['zope',],
-    tests_require = ['zope.testing'],
+    test_suite = 'zope.testbrowser.tests',
+    tests_require = tests_require,
     install_requires = [
         # mechanize 0.2.0 folds in ClientForm, makes incompatible API changes
         'mechanize>=0.2.0',
@@ -56,26 +61,15 @@
         'pytz',
         ],
     extras_require = {
-        'test': [
-            'zope.browserpage',
-            'zope.browserresource',
-            'zope.component',
-            'zope.container',
-            'zope.principalregistry',
-            'zope.ptresource',
-            'zope.publisher',
-            'zope.security',
-            'zope.site',
-            'zope.traversing',
-            'zope.app.appsetup',
-            'zope.app.publication',
-            'zope.app.testing >= 3.8.1',
+        'test': tests_require,
+        'test_bbb': [
+            'zope.testbrowser [test,zope-functional-testing]',
             ],
         'zope-functional-testing': [
-            'zope.app.testing',
+            'zope.app.testing >= 3.9.0dev',
             ],
         'wsgi': [
-            'wsgi_intercept',
+            'WebTest',
             ]
         },
     include_package_data = True,

Modified: zope.testbrowser/trunk/src/zope/testbrowser/README.txt
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/README.txt	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/README.txt	2011-03-14 08:52:34 UTC (rev 120900)
@@ -21,18 +21,24 @@
 ~~~~~~~~~~~~~~~~~
 
 There is also a special version of the ``Browser`` class which uses
-`wsgi_intercept`_ and can be used to do functional testing of WSGI
-applications, it can be imported from ``zope.testbrowser.wsgi``:
+`WebTest`_ and can be used to do functional testing of WSGI
+applications. It can be imported from ``zope.testbrowser.wsgi``:
 
     >>> from zope.testbrowser.wsgi import Browser
-    >>> browser = Browser()
+    >>> from wsgiref.simple_server import demo_app
+    >>> browser = Browser('http://localhost/', wsgi_app=demo_app)
+    >>> print browser.contents
+    Hello world!
+    ...
 
-.. _`wsgi_intercept`: http://pypi.python.org/pypi/wsgi_intercept
+.. _`WebTest`: http://pypi.python.org/pypi/WebTest
 
 To use this browser you have to:
 
   * use the `wsgi` extra of the ``zope.testbrowser`` egg,
 
+You can also use it with zope layers by:
+
   * write a subclass of ``zope.testbrowser.wsgi.Layer`` and override the
     ``make_wsgi_app`` method,
 
@@ -47,23 +53,17 @@
 
 Where ``simple_app`` is the callable of your WSGI application.
 
-Zope 3 Test Browser
-~~~~~~~~~~~~~~~~~~~
+Bowser Usage
+------------
 
-There is also a special version of the ``Browser`` class used to do functional
-testing of Zope 3 applications, it can be imported from
-``zope.testbrowser.testing``:
+We will test this browser against a WSGI test application:
 
-    >>> from zope.testbrowser.testing import Browser
-    >>> browser = Browser()
+    >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication
+    >>> wsgi_app = WSGITestApplication()
 
-Bowser Usage
-------------
+An initial page to load can be passed to the ``Browser`` constructor:
 
-All browsers are used the same way.  An initial page to load can be passed
-to the ``Browser`` constructor:
-
-    >>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
+    >>> browser = Browser('http://localhost/@@/testbrowser/simple.html', wsgi_app=wsgi_app)
     >>> browser.url
     'http://localhost/@@/testbrowser/simple.html'
 
@@ -183,7 +183,6 @@
     Status: 200 OK
     Content-Length: 123
     Content-Type: text/html;charset=utf-8
-    X-Powered-By: Zope (www.zope.org), Python (www.python.org)
 
 Or as a mapping:
 
@@ -1242,7 +1241,7 @@
 If the HTML page contains only one form, no arguments to `getForm` are
 needed:
 
-    >>> oneform = Browser()
+    >>> oneform = Browser(wsgi_app=wsgi_app)
     >>> oneform.open('http://localhost/@@/testbrowser/oneform.html')
     >>> form = oneform.getForm()
 
@@ -1261,63 +1260,51 @@
 method that allows a request body to be supplied.  This method is particularly
 helpful when testing Ajax methods.
 
-Let's visit a page that echos it's request:
+Let's visit a page that echos some interesting values from it's request:
 
-    >>> browser.open('http://localhost/@@echo.html')
-    >>> print browser.contents,
+    >>> browser.open('http://localhost/echo.html')
+    >>> print browser.contents
+    HTTP_ACCEPT_LANGUAGE: en-US
+    HTTP_CONNECTION: close
+    HTTP_HOST: localhost
     HTTP_USER_AGENT: Python-urllib/2.4
-    HTTP_CONNECTION: close
-    HTTP_COOKIE:
-    REMOTE_ADDR: 127.0.0.1
-    HTTP_ACCEPT_LANGUAGE: en-US
+    PATH_INFO: /echo.html
     REQUEST_METHOD: GET
-    HTTP_HOST: localhost
-    PATH_INFO: /@@echo.html
-    SERVER_PROTOCOL: HTTP/1.1
-    QUERY_STRING:
     Body: ''
 
 Now, we'll try a post.  The post method takes a URL, a data string,
 and an optional content type.  If we just pass a string, then
 a URL-encoded query string is assumed:
 
-    >>> browser.post('http://localhost/@@echo.html', 'x=1&y=2')
-    >>> print browser.contents,
+    >>> browser.post('http://localhost/echo.html', 'x=1&y=2')
+    >>> print browser.contents
     CONTENT_LENGTH: 7
+    CONTENT_TYPE: application/x-www-form-urlencoded
+    HTTP_ACCEPT_LANGUAGE: en-US
+    HTTP_CONNECTION: close
+    HTTP_HOST: localhost
     HTTP_USER_AGENT: Python-urllib/2.4
-    HTTP_CONNECTION: close
-    HTTP_COOKIE:
-    REMOTE_ADDR: 127.0.0.1
-    HTTP_ACCEPT_LANGUAGE: en-US
-    y: 2
+    PATH_INFO: /echo.html
     REQUEST_METHOD: POST
-    HTTP_HOST: localhost
-    PATH_INFO: /@@echo.html
-    CONTENT_TYPE: application/x-www-form-urlencoded
-    SERVER_PROTOCOL: HTTP/1.1
-    QUERY_STRING:
     x: 1
+    y: 2
     Body: ''
 
-
 The body is empty because it is consumed to get form data.
 
 We can pass a content-type explicitly:
 
-    >>> browser.post('http://localhost/@@echo.html',
+    >>> browser.post('http://localhost/echo.html',
     ...              '{"x":1,"y":2}', 'application/x-javascript')
-    >>> print browser.contents,
+    >>> print browser.contents
     CONTENT_LENGTH: 13
+    CONTENT_TYPE: application/x-javascript
+    HTTP_ACCEPT_LANGUAGE: en-US
+    HTTP_CONNECTION: close
+    HTTP_HOST: localhost
     HTTP_USER_AGENT: Python-urllib/2.4
-    HTTP_CONNECTION: close
-    HTTP_COOKIE:
-    REMOTE_ADDR: 127.0.0.1
-    HTTP_ACCEPT_LANGUAGE: en-US
+    PATH_INFO: /echo.html
     REQUEST_METHOD: POST
-    HTTP_HOST: localhost
-    PATH_INFO: /@@echo.html
-    CONTENT_TYPE: application/x-javascript
-    SERVER_PROTOCOL: HTTP/1.1
     Body: '{"x":1,"y":2}'
 
 Here, the body is left in place because it isn't form data.
@@ -1338,11 +1325,11 @@
     True
 
 
-Handling Errors when using Zope 3's Publisher
----------------------------------------------
+Handling Errors
+---------------
 
-A very useful feature of the publisher is the automatic graceful handling of
-application errors, such as invalid URLs:
+Often WSGI middleware or the application itself gracefully handle application
+errors, such as invalid URLs:
 
     >>> browser.open('http://localhost/invalid')
     Traceback (most recent call last):
@@ -1350,7 +1337,7 @@
     HTTPError: HTTP Error 404: Not Found
 
 Note that the above error was thrown by ``mechanize`` and not by the
-publisher.  For debugging purposes, however, it can be very useful to see the
+application.  For debugging purposes, however, it can be very useful to see the
 original exception caused by the application.  In those cases you can set the
 ``handleErrors`` property of the browser to ``False``.  It is defaulted to
 ``True``:
@@ -1358,22 +1345,21 @@
     >>> browser.handleErrors
     True
 
-So when we tell the publisher not to handle the errors,
+So when we tell the application not to handle the errors,
 
     >>> browser.handleErrors = False
 
-we get a different, Zope internal error:
+we get a different, internal error from the application:
 
     >>> browser.open('http://localhost/invalid')
     Traceback (most recent call last):
     ...
-    NotFound: Object: <zope.site.folder.Folder object at ...>,
-              name: u'invalid'
+    NotFound: /invalid
 
-NB: Setting the handleErrors attribute to False will only change
-    anything if the http server you're testing is using Zope 3's
-    publisher or can otherwise respond appropriately to an
-    'X-zope-handle-errors' header in requests.
+NB: Setting the handleErrors attribute to False will only change anything if
+    the WSGI application obeys the wsgi.handleErrors or paste.throw_errors
+    WSGI environment variables. i.e. it does not catch and handle the original
+    exception when these are set appropriately.
 
 When the testbrowser is raising HttpErrors, the errors still hit the test.
 Sometimes we don't want that to happen, in situations where there are edge
@@ -1401,8 +1387,7 @@
     >>> browser.open('http://localhost/invalid')
     Traceback (most recent call last):
     ...
-    NotFound: Object: <zope.site.folder.Folder object at ...>,
-        name: u'invalid'
+    NotFound: /invalid
 
     >>> browser.raiseHttpErrors = True
 
@@ -1432,3 +1417,20 @@
     Traceback (most recent call last):
     ...
     AttributeError: 'Link' object has no attribute 'nonexistant'
+
+
+HTTPS support
+-------------
+
+Depending on the scheme of the request the variable wsgi.url_scheme will be set
+correctly on the request:
+
+    >>> browser.open('http://localhost/echo_one.html?var=wsgi.url_scheme')
+    >>> print browser.contents
+    'http'
+    
+    >>> browser.open('https://localhost/echo_one.html?var=wsgi.url_scheme')
+    >>> print browser.contents
+    'https'
+
+see http://www.python.org/dev/peps/pep-3333/ for details.

Copied: zope.testbrowser/trunk/src/zope/testbrowser/connection.py (from rev 120899, zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/connection.py)
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/connection.py	                        (rev 0)
+++ zope.testbrowser/trunk/src/zope/testbrowser/connection.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -0,0 +1,96 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Base classes sometimes useful to implement browsers
+"""
+import cStringIO
+import httplib
+import mechanize
+import socket
+import sys
+import zope.testbrowser.browser
+
+
+class Response(object):
+    """``mechanize`` compatible response object."""
+
+    def __init__(self, content, headers, status, reason):
+        self.content = content
+        self.status = status
+        self.reason = reason
+        self.msg = httplib.HTTPMessage(cStringIO.StringIO(headers), 0)
+        self.content_as_file = cStringIO.StringIO(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 HTTPHandler(mechanize.HTTPHandler):
+
+    def _connect(self, *args, **kw):
+        raise NotImplementedError("implement")
+
+    def http_request(self, req):
+        # look at data and set content type
+        if req.has_data():
+            data = req.get_data()
+            if isinstance(data, dict):
+                req.add_data(data['body'])
+                req.add_unredirected_header('Content-type',
+                                            data['content-type'])
+        return mechanize.HTTPHandler.do_request_(self, req)
+
+    https_request = http_request
+
+    def http_open(self, req):
+        """Open an HTTP connection having a ``mechanize`` request."""
+        # Here we connect to the publisher.
+        if sys.version_info > (2, 6) and not hasattr(req, 'timeout'):
+            # Workaround mechanize incompatibility with Python
+            # 2.6. See: LP #280334
+            req.timeout = socket._GLOBAL_DEFAULT_TIMEOUT
+        return self.do_open(self._connect, req)
+
+    https_open = http_open
+
+class MechanizeBrowser(mechanize.Browser):
+    """Special ``mechanize`` browser using the Zope Publisher HTTP handler."""
+
+    default_schemes = ['http']
+    default_others = ['_http_error', '_http_default_error']
+    default_features = ['_redirect', '_cookies', '_referer', '_refresh',
+                        '_equiv', '_basicauth', '_digestauth']
+
+
+    def __init__(self, *args, **kws):
+        inherited_handlers = ['_unknown', '_http_error',
+            '_http_default_error', '_basicauth',
+            '_digestauth', '_redirect', '_cookies', '_referer',
+            '_refresh', '_equiv', '_gzip']
+
+        self.handler_classes = {"http": self._http_handler}
+        for name in inherited_handlers:
+            self.handler_classes[name] = mechanize.Browser.handler_classes[name]
+
+        kws['request_class'] = kws.get('request_class',
+                                       mechanize._request.Request)
+
+        mechanize.Browser.__init__(self, *args, **kws)
+
+    def _http_handler(self, *args, **kw):
+        return NotImplementedError("Try return a sub-class of PublisherHTTPHandler here")
+
+

Modified: zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/cookies.txt	2011-03-14 08:52:34 UTC (rev 120900)
@@ -9,9 +9,12 @@
 setting, and deleting the cookies that the browser is remembering for the
 current url, or for an explicitly provided URL.
 
-    >>> from zope.testbrowser.testing import Browser
-    >>> browser = Browser()
+    >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication
+    >>> from zope.testbrowser.wsgi import Browser
 
+    >>> wsgi_app = WSGITestApplication()
+    >>> browser = Browser(wsgi_app=wsgi_app)
+
 Initially the browser does not point to a URL, and the cookies cannot be used.
 
     >>> len(browser.cookies)
@@ -225,7 +228,7 @@
     ...     'http://localhost/set_cookie.html?name=max&value=min&'
     ...     'max-age=3000&&comment=silly+billy')
     >>> pprint.pprint(browser.cookies.getinfo('max')) # doctest: +ELLIPSIS
-    {'comment': 'silly%20billy',
+    {'comment': '"silly billy"',
      'commenturl': None,
      'domain': 'localhost.local',
      'expires': datetime.datetime(..., tzinfo=<UTC>),
@@ -254,7 +257,7 @@
       'port': None,
       'secure': False,
       'value': 'bar'},
-     {'comment': 'silly%20billy',
+     {'comment': '"silly billy"',
       'commenturl': None,
       'domain': 'localhost.local',
       'expires': datetime.datetime(..., tzinfo=<UTC>),
@@ -312,11 +315,6 @@
     >>> sorted(browser.cookies.keys())
     ['foo', 'max', 'sha', 'va', 'wow']
 
-    >>> import zope.site.folder
-    >>> getRootFolder()['inner'] = zope.site.folder.Folder()
-    >>> getRootFolder()['inner']['path'] = zope.site.folder.Folder()
-    >>> import transaction
-    >>> transaction.commit()
     >>> browser.open('http://localhost/inner/get_cookie.html')
     >>> print browser.contents # has gewgaw
     foo: bar

Modified: zope.testbrowser/trunk/src/zope/testbrowser/fixed-bugs.txt
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/fixed-bugs.txt	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/fixed-bugs.txt	2011-03-14 08:52:34 UTC (rev 120900)
@@ -5,6 +5,9 @@
 This file includes tests for bugs that were found and then fixed that don't fit
 into the more documentation-centric sections above.
 
+    >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication
+    >>> from zope.testbrowser.wsgi import Browser
+    >>> wsgi_app = WSGITestApplication()
 
 Unicode URLs
 ============
@@ -24,13 +27,11 @@
 
 The tests below failed before the change was put in place.
 
-    >>> from zope.testbrowser.testing import Browser
-    >>> browser = Browser()
+    >>> browser = Browser(wsgi_app=wsgi_app)
     >>> browser.addHeader('Cookie', 'test')
     >>> browser.open(u'http://localhost/@@/testbrowser/simple.html')
 
-    >>> from zope.testbrowser.testing import Browser
-    >>> browser = Browser()
+    >>> browser = Browser(wsgi_app=wsgi_app)
     >>> browser.addHeader(u'Cookie', 'test')
     >>> browser.open('http://localhost/@@/testbrowser/simple.html')
 

Modified: zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/__init__.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -11,43 +11,3 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-
-class View:
-
-    def __init__(self, context, request):
-        self.context = context
-        self.request = request
-
-class Echo(View):
-    """Simply echo the contents of the request"""
-
-    def __call__(self):
-        return ('\n'.join('%s: %s' % x for x in self.request.items()) +
-            '\nBody: %r' % self.request.bodyStream.read())
-
-class GetCookie(View):
-    """Gets cookie value"""
-
-    def __call__(self):
-        return '\n'.join(
-            ('%s: %s' % (k, v)) for k, v in sorted(
-                self.request.cookies.items()))
-
-class SetCookie(View):
-    """Sets cookie value"""
-
-    def __call__(self):
-        self.request.response.setCookie(
-            **dict((str(k), str(v)) for k, v in self.request.form.items()))
-
-
-class SetStatus(View):
-    """Sets HTTP status"""
-
-    def __call__(self):
-        status = self.request.get('status')
-        if status:
-            self.request.response.setStatus(int(status))
-            return 'Just set a status of %s' % status
-        else:
-            return 'Everything fine'

Modified: zope.testbrowser/trunk/src/zope/testbrowser/ftests/controls.html
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/controls.html	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/controls.html	2011-03-14 08:52:34 UTC (rev 120900)
@@ -7,32 +7,32 @@
 
       <div>
         <label for="text-value">Text Control</label>
-        <em tal:condition="request/text-value|nothing"
-            tal:content="request/text-value"></em>
+        <em tal:condition="request/params/text-value|nothing"
+            tal:content="request/params/text-value"></em>
         <input type="text" name="text-value" id="text-value" 
                value="Some Text" />
       </div>
 
       <div>
         <label for="password-value">Password Control</label>
-        <em tal:condition="request/password-value|nothing"
-            tal:content="request/password-value"></em>
+        <em tal:condition="request/params/password-value|nothing"
+            tal:content="request/params/password-value"></em>
         <input type="password" name="password-value" id="password-value"
                value="Password" />
       </div>
 
       <div>
         <label for="hidden-value">Hidden Control</label> (label: hee hee)
-        <em tal:condition="request/hidden-value|nothing"
-            tal:content="request/hidden-value"></em>
+        <em tal:condition="request/params/hidden-value|nothing"
+            tal:content="request/params/hidden-value"></em>
         <input type="hidden" name="hidden-value" id="hidden-value"
                value="Hidden" />
       </div>
 
       <div>
         <label for="textarea-value">Text Area Control</label>
-        <em tal:condition="request/textarea-value|nothing"
-            tal:content="request/textarea-value"></em>
+        <em tal:condition="request/params/textarea-value|nothing"
+            tal:content="request/params/textarea-value"></em>
         <textarea name="textarea-value" id="textarea-value">
           Text inside
           area!
@@ -41,15 +41,15 @@
 
       <div>
         <label for="file-value">File Control</label>
-        <em tal:condition="request/file-value|nothing"
-            tal:content="request/file-value"></em>
+        <em tal:condition="request/params/file-value|nothing"
+            tal:content="request/params/file-value"></em>
         <input type="file" name="file-value" id="file-value" />
       </div>
 
       <div>
         <label for="single-select-value">Single Select Control</label>
-        <em tal:condition="request/single-select-value|nothing"
-            tal:content="request/single-select-value"></em>
+        <em tal:condition="request/params/single-select-value|nothing"
+            tal:content="request/params/single-select-value"></em>
         <select name="single-select-value" id="single-select-value">
           <option value="1">Uno</option>
           <option value="2">Dos</option>
@@ -59,8 +59,8 @@
 
       <div>
         <label for="multi-select-value">Multiple Select Control</label>
-        <em tal:condition="request/multi-select-value|nothing"
-            tal:content="request/multi-select-value"></em>
+        <em tal:condition="request/params/multi-select-value|nothing"
+            tal:content="request/params/multi-select-value"></em>
         <select name="multi-select-value" id="multi-select-value"
                 multiple="multiple">
           <option value="1">Un</option>
@@ -70,8 +70,8 @@
       </div>
 
       <div>
-        <em tal:condition="request/single-unvalued-checkbox-value|nothing"
-            tal:content="request/single-unvalued-checkbox-value"></em>
+        <em tal:condition="request/params/single-unvalued-checkbox-value|nothing"
+            tal:content="request/params/single-unvalued-checkbox-value"></em>
         <input type="checkbox" name="single-unvalued-checkbox-value" 
                id="single-unvalued-checkbox" checked="checked" />
         <label for="single-unvalued-checkbox">Single Unvalued Checkbox</label>
@@ -79,8 +79,8 @@
 
       <div>
         <em tal:condition="
-            request/single-disabled-unvalued-checkbox-value|nothing"
-            tal:content="request/single-disabled-unvalued-checkbox-value"></em>
+            request/params/single-disabled-unvalued-checkbox-value|nothing"
+            tal:content="request/params/single-disabled-unvalued-checkbox-value"></em>
         <input type="checkbox" name="single-disabled-unvalued-checkbox-value" 
                id="single-disabled-unvalued-checkbox" checked="checked"
                disabled="disabled" />
@@ -90,8 +90,8 @@
       </div>
 
       <div>
-        <em tal:condition="request/single-valued-checkbox-value|nothing"
-            tal:content="request/single-valued-checkbox-value"></em>
+        <em tal:condition="request/params/single-valued-checkbox-value|nothing"
+            tal:content="request/params/single-valued-checkbox-value"></em>
         <label><input type="checkbox" name="single-valued-checkbox-value" 
                       value="1" checked="checked" />Single Valued Checkbox
         </label>
@@ -99,8 +99,8 @@
 
       <div>
         (Multi checkbox: options have the labels)
-        <em tal:condition="request/multi-checkbox-value|nothing"
-            tal:content="request/multi-checkbox-value"></em>
+        <em tal:condition="request/params/multi-checkbox-value|nothing"
+            tal:content="request/params/multi-checkbox-value"></em>
         <label><input type="checkbox" name="multi-checkbox-value" value="1" 
                       checked="checked" /> One</label>
         <input type="checkbox" name="multi-checkbox-value" value="2" 
@@ -114,8 +114,8 @@
 
       <div>
         (Radio: options have the labels)
-        <em tal:condition="request/radio-value|nothing"
-            tal:content="request/radio-value"></em>
+        <em tal:condition="request/params/radio-value|nothing"
+            tal:content="request/params/radio-value"></em>
         <label><input type="radio" name="radio-value" value="1" />Ein</label>
         <input type="radio" name="radio-value" id="radio-value-2" value="2"
                checked="checked" />
@@ -127,18 +127,18 @@
 
       <div>
         <label for="image-value">Image Control</label>
-        <em tal:condition="request/image-value.x|nothing"
-            tal:content="request/image-value.x"></em>
-        <em tal:condition="request/image-value.y|nothing"
-            tal:content="request/image-value.y"></em>
+        <em tal:condition="request/params/image-value.x|nothing"
+            tal:content="request/params/image-value.x"></em>
+        <em tal:condition="request/params/image-value.y|nothing"
+            tal:content="request/params/image-value.y"></em>
         <input type="image" name="image-value" id="image-value"
                src="zope3logo.gif" />
       </div>
 
       <div>
         <label for="submit-value">Standard Submit Control</label>
-        <em tal:condition="request/submit-value|nothing"
-            tal:content="request/submit-value"></em>
+        <em tal:condition="request/params/submit-value|nothing"
+            tal:content="request/params/submit-value"></em>
         <input type="submit" name="submit-value" id="submit-value"
                value="Submit This" />
       </div>

Modified: zope.testbrowser/trunk/src/zope/testbrowser/ftests/forms.html
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/forms.html	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/forms.html	2011-03-14 08:52:34 UTC (rev 120900)
@@ -3,8 +3,8 @@
 
     <h1>Forms Tests</h1>
 
-    <em tal:condition="request/text-value|nothing"
-        tal:content="request/text-value" />
+    <em tal:condition="request/params/text-value|nothing"
+        tal:content="request/params/text-value" />
 
     <form id="1" name="one" action="forms.html">
       <input type="text" name="text-value" value="First Text" />
@@ -28,8 +28,8 @@
       <label for="text-value-4">Text Control</label>
       <input type="text" name="text-value" id="text-value-4"
              value="Fourth Text" />
-      <em tal:condition="python: 'hidden-4' in request.form and
-                                 'submit-4' not in request.form"
+      <em tal:condition="python: 'hidden-4' in request.params and
+                                 'submit-4' not in request.params"
         >Submitted without the submit button.</em>
       <input type="submit" name="submit-4" value="Don't Submit Me" />
       <input type="hidden" name="hidden-4" value="marker" />

Deleted: zope.testbrowser/trunk/src/zope/testbrowser/ftests/ftesting.zcml
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/ftesting.zcml	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/ftesting.zcml	2011-03-14 08:52:34 UTC (rev 120900)
@@ -1,63 +0,0 @@
-<configure
-  xmlns="http://namespaces.zope.org/zope"
-  xmlns:browser="http://namespaces.zope.org/browser"
-  i18n_domain="zope"
-  package="zope.testbrowser"
-  >
-
-  <!-- This file is used for functional testing setup -->
-
-  <include package="zope.browserpage" file="meta.zcml" />
-  <include package="zope.browserresource" file="meta.zcml" />
-  <include package="zope.component" file="meta.zcml" />
-  <include package="zope.security" file="meta.zcml" />
-  <include package="zope.app.publication" file="meta.zcml" />
-
-  <include package="zope.browserresource" />
-  <include package="zope.container" />
-  <include package="zope.principalregistry" />
-  <include package="zope.ptresource" />
-  <include package="zope.publisher" />
-  <include package="zope.security" />
-  <include package="zope.site" />
-  <include package="zope.traversing" />
-
-  <include package="zope.app.appsetup" />
-  <include package="zope.app.publication" />
-
-  <securityPolicy
-      component="zope.security.simplepolicies.PermissiveSecurityPolicy" />
-
-  <browser:page
-     name="echo.html"
-     for="*"
-     class=".ftests.Echo"
-     permission="zope.Public"
-     />
-
-  <browser:page
-     name="set_cookie.html"
-     for="*"
-     class=".ftests.SetCookie"
-     permission="zope.Public"
-     />
-
-  <browser:page
-     name="get_cookie.html"
-     for="*"
-     class=".ftests.GetCookie"
-     permission="zope.Public"
-     />
-
-  <browser:page
-     name="set_status.html"
-     for="*"
-     class=".ftests.SetStatus"
-     permission="zope.Public"
-     />
-
-  <browser:resourceDirectory
-      name="testbrowser"
-      directory="ftests" />
-
-</configure>

Modified: zope.testbrowser/trunk/src/zope/testbrowser/ftests/navigate.html
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/navigate.html	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/navigate.html	2011-03-14 08:52:34 UTC (rev 120900)
@@ -3,8 +3,8 @@
 
     <h1>Navigation Tests</h1>
 
-    <p tal:condition="request/message|nothing">
-      Message: <em tal:content="request/message">Message</em>
+    <p tal:condition="request/params/message|nothing">
+      Message: <em tal:content="request/params/message">Message</em>
     </p>
 
     <a href="navigate.html?message=By+Link+Text">Link Text</a>

Modified: zope.testbrowser/trunk/src/zope/testbrowser/ftests/textarea.html
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/textarea.html	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/textarea.html	2011-03-14 08:52:34 UTC (rev 120900)
@@ -6,8 +6,8 @@
     <form action="textarea.html" method="post">
       <div>
         <label for="textarea-value">Text Area Control</label>
-        <em tal:condition="request/textarea-value|nothing"
-            tal:content="request/textarea-value"></em>
+        <em tal:condition="request/params/textarea-value|nothing"
+            tal:content="request/params/textarea-value"></em>
           <textarea name="textarea-value" id="textarea-value">&lt;block&gt;
   &lt;feed/&gt;
   &amp;

Copied: zope.testbrowser/trunk/src/zope/testbrowser/ftests/wsgitestapp.py (from rev 120899, zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/wsgitestapp.py)
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/ftests/wsgitestapp.py	                        (rev 0)
+++ zope.testbrowser/trunk/src/zope/testbrowser/ftests/wsgitestapp.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -0,0 +1,140 @@
+##############################################################################
+#
+# Copyright (c) 2010 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""A minimal WSGI application used as a test fixture."""
+
+import os
+import mimetypes
+from datetime import datetime
+
+from webob import Request, Response
+from zope.pagetemplate.pagetemplatefile import PageTemplateFile
+
+class NotFound(Exception):
+    pass
+
+_HERE = os.path.dirname(__file__)
+
+class MyPageTemplateFile(PageTemplateFile):
+
+    def pt_getContext(self, args, *extra_args, **kw):
+        namespace = super(MyPageTemplateFile, self).pt_getContext(args, *extra_args, **kw)
+        namespace['request'] = args[0]
+        return namespace
+
+class WSGITestApplication(object):
+
+    def __call__(self, environ, start_response):
+        req = Request(environ)
+        handler = {'/set_status.html': set_status,
+                   '/echo.html': echo,
+                   '/echo_one.html': echo_one,
+                   '/set_header.html': set_header,
+                   '/set_cookie.html': set_cookie,
+                   '/get_cookie.html': get_cookie,
+                   '/inner/set_cookie.html': set_cookie,
+                   '/inner/get_cookie.html': get_cookie,
+                   '/inner/path/set_cookie.html': set_cookie,
+                   '/inner/path/get_cookie.html': get_cookie,
+                   }.get(req.path_info)
+        if handler is None and req.path_info.startswith('/@@/testbrowser/'):
+            handler = handle_resource
+        if handler is None:
+            handler = handle_notfound
+        try:
+            resp = handler(req)
+        except Exception, exc:
+            if not environ.get('wsgi.handleErrors', True):
+                raise
+            resp = Response()
+            status = 500
+            if isinstance(exc, NotFound):
+                status = 404
+            resp.status = status
+        return resp(environ, start_response)
+
+def handle_notfound(req):
+    raise NotFound(req.path_info)
+
+def handle_resource(req):
+    filename = req.path_info.split('/')[-1]
+    type, _ = mimetypes.guess_type(filename)
+    path = os.path.join(_HERE, filename)
+    if type == 'text/html':
+        pt = MyPageTemplateFile(path)
+        contents = pt(req)
+    else:
+        contents = open(path, 'r').read()
+    return Response(contents, content_type=type)
+
+def get_cookie(req):
+    cookies = ['%s: %s' % i for i in sorted(req.cookies.items())]
+    return Response('\n'.join(cookies))
+    
+def set_cookie(req):
+    cookie_parms = {'path': None}
+    cookie_parms.update(dict((str(k), str(v)) for k, v in req.params.items()))
+    name = cookie_parms.pop('name')
+    value = cookie_parms.pop('value')
+    if 'max-age' in cookie_parms:
+        cookie_parms['max_age'] = int(cookie_parms.pop('max-age'))
+    if 'expires' in cookie_parms:
+        cookie_parms['expires'] = datetime.strptime(cookie_parms.pop('expires'), '%a, %d %b %Y %H:%M:%S GMT')
+    resp = Response()
+    resp.set_cookie(name, value, **cookie_parms)
+    return resp
+
+def set_header(req):
+    resp = Response()
+    body = [u"Set Headers:"]
+    for k, v in sorted(req.params.items()):
+        body.extend([k, v]) 
+        resp.headers.add(k, v)
+    resp.unicode_body = u'\n'.join(body)
+    return resp
+
+_interesting_environ = ('CONTENT_LENGTH',
+                        'CONTENT_TYPE',
+                        'HTTP_ACCEPT_LANGUAGE',
+                        'HTTP_CONNECTION',
+                        'HTTP_HOST',
+                        'HTTP_USER_AGENT',
+                        'PATH_INFO',
+                        'REQUEST_METHOD')
+
+def echo(req):
+    items = []
+    for k in _interesting_environ:
+        v = req.environ.get(k, None)
+        if v is None:
+            continue
+        items.append('%s: %s' % (k, v))
+    items.extend('%s: %s' % x for x in sorted(req.params.items())) 
+    if req.method == 'POST' and req.content_type == 'application/x-www-form-urlencoded':
+        body = ''
+    else:
+        body = req.body
+    items.append('Body: %r' % body)
+    return Response('\n'.join(items))
+
+def echo_one(req):
+    resp = repr(req.environ.get(req.params['var']))
+    return Response(resp)
+
+def set_status(req):
+    status = req.params.get('status')
+    if status:
+        resp = Response('Just set a status of %s' % status)
+        resp.status = int(status)
+        return resp
+    return Response('Everything fine')

Modified: zope.testbrowser/trunk/src/zope/testbrowser/testing.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/testing.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/testing.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -11,161 +11,21 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""Zope 3-specific testing code
+"""BBB for Zope 3-specific testing code
 """
-import cStringIO
-import httplib
-import mechanize
-import socket
-import sys
-import zope.testbrowser.browser
 
+from zope.testbrowser.connection import Response as PublisherResponse
 
-class PublisherConnection(object):
-    """A ``mechanize`` compatible connection object."""
+try:
+    import zope.app.testing
+    have_zope_app_testing = True
+except ImportError:
+    have_zope_app_testing = False
 
-    def __init__(self, host, timeout=None):
-        from zope.app.testing.functional import HTTPCaller
-        self.caller = HTTPCaller()
-        self.host = host
+if have_zope_app_testing:
+    from zope.app.testing.testbrowser import (PublisherConnection,
+                                              PublisherHTTPHandler,
+                                              PublisherMechanizeBrowser,
+                                              Browser)
 
-    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
-        if sys.version_info >= (2,5):
-            handle_errors_key = 'X-Zope-Handle-Errors'
-        else:
-            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.
-        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
-        # ``HTTPCaller`` wants.
-        request_string = (method + ' ' + url + ' HTTP/1.1\n'
-                          + headers + '\n' + body)
-        self.response = self.caller(request_string, handle_errors)
-
-    def getresponse(self):
-        """Return a ``mechanize`` compatible response.
-
-        The goal of ths method is to convert the Zope Publisher's reseponse to
-        a ``mechanize`` compatible response, which is also understood by
-        mechanize.
-        """
-        real_response = self.response._response
-        status = real_response.getStatus()
-        reason = real_response._reason # XXX add a getReason method
-
-        headers = real_response.getHeaders()
-        headers.sort()
-        headers.insert(0, ('Status', real_response.getStatusString()))
-        headers = '\r\n'.join('%s: %s' % h for h in headers)
-        content = real_response.consumeBody()
-        return PublisherResponse(content, headers, status, reason)
-
-
-class PublisherResponse(object):
-    """``mechanize`` compatible response object."""
-
-    def __init__(self, content, headers, status, reason):
-        self.content = content
-        self.status = status
-        self.reason = reason
-        self.msg = httplib.HTTPMessage(cStringIO.StringIO(headers), 0)
-        self.content_as_file = cStringIO.StringIO(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(mechanize.HTTPHandler):
-    """Special HTTP handler to use the Zope Publisher."""
-
-    def http_request(self, req):
-        # look at data and set content type
-        if req.has_data():
-            data = req.get_data()
-            if isinstance(data, dict):
-                req.add_data(data['body'])
-                req.add_unredirected_header('Content-type',
-                                            data['content-type'])
-        return mechanize.HTTPHandler.do_request_(self, req)
-
-    https_request = http_request
-
-    def http_open(self, req):
-        """Open an HTTP connection having a ``mechanize`` request."""
-        # Here we connect to the publisher.
-        if sys.version_info > (2, 6) and not hasattr(req, 'timeout'):
-            # Workaround mechanize incompatibility with Python
-            # 2.6. See: LP #280334
-            req.timeout = socket._GLOBAL_DEFAULT_TIMEOUT
-        return self.do_open(PublisherConnection, req)
-
-    https_open = http_open
-
-
-class PublisherMechanizeBrowser(mechanize.Browser):
-    """Special ``mechanize`` browser using the Zope Publisher HTTP handler."""
-
-    default_schemes = ['http']
-    default_others = ['_http_error', '_http_default_error']
-    default_features = ['_redirect', '_cookies', '_referer', '_refresh',
-                        '_equiv', '_basicauth', '_digestauth']
-
-    def __init__(self, *args, **kws):
-        inherited_handlers = ['_unknown', '_http_error',
-            '_http_default_error', '_basicauth',
-            '_digestauth', '_redirect', '_cookies', '_referer',
-            '_refresh', '_equiv', '_gzip']
-
-        self.handler_classes = {"http": PublisherHTTPHandler}
-        for name in inherited_handlers:
-            self.handler_classes[name] = mechanize.Browser.handler_classes[name]
-
-        kws['request_class'] = kws.get('request_class',
-                                       mechanize._request.Request)
-
-        mechanize.Browser.__init__(self, *args, **kws)
-
-
-class Browser(zope.testbrowser.browser.Browser):
-    """A Zope `testbrowser` Browser that uses the Zope Publisher."""
-
-    def __init__(self, url=None):
-        mech_browser = PublisherMechanizeBrowser()
-        super(Browser, self).__init__(url=url, mech_browser=mech_browser)
+del have_zope_app_testing

Modified: zope.testbrowser/trunk/src/zope/testbrowser/tests/helper.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/tests/helper.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/tests/helper.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -37,4 +37,7 @@
     (re.compile(r'Host: localhost'), 'Connection: close'),
     (re.compile(r'Content-Type: '), 'Content-type: '),
     (re.compile(r'Content-Disposition: '), 'Content-disposition: '),
+    (re.compile(r'; charset=UTF-8'), ';charset=utf-8'),
+    # webtest seems to expire cookies one second before the date set in set_cookie
+    (re.compile(r"'expires': datetime.datetime\(2029, 12, 31, 23, 59, 59, tzinfo=<UTC>\),"), "'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=<UTC>),"),
     ])

Copied: zope.testbrowser/trunk/src/zope/testbrowser/tests/test_bbb.py (from rev 120899, zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/test_bbb.py)
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/tests/test_bbb.py	                        (rev 0)
+++ zope.testbrowser/trunk/src/zope/testbrowser/tests/test_bbb.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -0,0 +1,24 @@
+##############################################################################
+#
+# Copyright (c) 2011 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import unittest
+
+class TestZopeAppTesting(unittest.TestCase):
+
+    def test_import(self):
+        try:
+            import zope.app.testing
+        except ImportError:
+            return
+        from zope.testbrowser.testing import Browser
+        browser = Browser()

Modified: zope.testbrowser/trunk/src/zope/testbrowser/tests/test_browser.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/tests/test_browser.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/tests/test_browser.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -435,3 +435,6 @@
     return doctest.DocTestSuite(
         checker=zope.testbrowser.tests.helper.checker,
         optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)
+
+# additional_tests is for setuptools "setup.py test" support
+additional_tests = test_suite

Modified: zope.testbrowser/trunk/src/zope/testbrowser/tests/test_doctests.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/tests/test_doctests.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/tests/test_doctests.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -14,28 +14,26 @@
 import doctest
 import pkg_resources
 import unittest
-import zope.app.testing.functional
 
+import zope.testbrowser.ftests.wsgitestapp
+import zope.testbrowser.wsgi
 
-TestBrowserLayer = zope.app.testing.functional.ZCMLLayer(
-    pkg_resources.resource_filename(
-        'zope.testbrowser', 'ftests/ftesting.zcml'),
-    __name__, 'TestBrowserLayer', allow_teardown=True)
-
-
 def test_suite():
     flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
-    suite = zope.app.testing.functional.FunctionalDocFileSuite(
+
+    suite = doctest.DocFileSuite(
         'README.txt',
         'cookies.txt',
         'fixed-bugs.txt',
         optionflags=flags,
         checker=zope.testbrowser.tests.helper.checker,
         package='zope.testbrowser')
-    suite.layer = TestBrowserLayer
 
     wire = doctest.DocFileSuite('over_the_wire.txt', optionflags=flags,
                                 package='zope.testbrowser')
     wire.level = 2
 
     return unittest.TestSuite((suite, wire))
+
+# additional_tests is for setuptools "setup.py test" support
+additional_tests = test_suite

Modified: zope.testbrowser/trunk/src/zope/testbrowser/tests/test_wsgi.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/tests/test_wsgi.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/tests/test_wsgi.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -13,32 +13,98 @@
 ##############################################################################
 
 import unittest
+from wsgiref.simple_server import demo_app
+
 import zope.testbrowser.wsgi
+from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication
 
 
-# Copied from PEP #333
-def simple_app(environ, start_response):
-    """Simplest possible application object"""
-    status = '200 OK'
-    response_headers = [('Content-type', 'text/plain')]
-    start_response(status, response_headers)
-    return ['Hello world!\n']
-
-
 class SimpleLayer(zope.testbrowser.wsgi.Layer):
 
     def make_wsgi_app(self):
-        return simple_app
+        return demo_app
 
 SIMPLE_LAYER = SimpleLayer()
 
 
-class TestWSGI(unittest.TestCase):
+class TestBrowser(unittest.TestCase):
 
-    layer = SIMPLE_LAYER
+    def test_allowed_domains(self):
+        browser = zope.testbrowser.wsgi.Browser(wsgi_app=demo_app)
+        # external domains are not allowed
+        self.assertRaises(zope.testbrowser.wsgi.HostNotAllowed, browser.open, 'http://www.google.com')
+        self.assertRaises(zope.testbrowser.wsgi.HostNotAllowed, browser.open, 'https://www.google.com')
+        # internal ones are
+        browser.open('http://localhost')
+        self.assertTrue(browser.contents.startswith('Hello world!\n'))
+        browser.open('http://127.0.0.1')
+        self.assertTrue(browser.contents.startswith('Hello world!\n'))
+        # as are example ones
+        browser.open('http://example.com')
+        self.assertTrue(browser.contents.startswith('Hello world!\n'))
+        browser.open('http://example.net')
+        self.assertTrue(browser.contents.startswith('Hello world!\n'))
+        # and subdomains of example
+        browser.open('http://foo.example.com')
+        self.assertTrue(browser.contents.startswith('Hello world!\n'))
+        browser.open('http://bar.example.net')
+        self.assertTrue(browser.contents.startswith('Hello world!\n'))
 
-    def test_(self):
+
+class TestWSGILayer(unittest.TestCase):
+
+    def setUp(self):
+        # test the layer without depending on zope.testrunner
+        SIMPLE_LAYER.setUp()
+
+    def tearDown(self):
+        SIMPLE_LAYER.tearDown()
+
+    def test_layer(self):
+        """When the layer is setup, the wsgi_app argument is unnecessary"""
         browser = zope.testbrowser.wsgi.Browser()
         browser.open('http://localhost')
-        self.assertEqual('Hello world!\n', browser.contents)
+        self.assertTrue(browser.contents.startswith('Hello world!\n'))
         # XXX test for authorization header munging is missing
+
+    def test_app_property(self):
+        # The layer has a .app property where the application under test is available
+        self.assertTrue(SIMPLE_LAYER.get_app() is demo_app)
+
+    def test_there_can_only_be_one(self):
+        another_layer = SimpleLayer()
+        # The layer has a .app property where the application under test is available
+        self.assertRaises(AssertionError, another_layer.setUp)
+
+class TestAuthorizationMiddleware(unittest.TestCase):
+
+    def setUp(self):
+        app = WSGITestApplication()
+        self.unwrapped_browser = zope.testbrowser.wsgi.Browser(wsgi_app=app)
+        app = zope.testbrowser.wsgi.AuthorizationMiddleware(app)
+        self.browser = zope.testbrowser.wsgi.Browser(wsgi_app=app)
+
+    def test_unwanted_headers(self):
+        #x-powered-by and x-content-type-warning are filtered
+        url = 'http://localhost/set_header.html?x-other=another&x-powered-by=zope&x-content-type-warning=bar'
+        self.browser.open(url)
+        self.assertEquals(self.browser.headers['x-other'], 'another')
+        self.assertTrue('x-other' in self.browser.headers)
+        self.assertFalse('x-powered-by' in self.browser.headers)
+        self.assertFalse('x-content-type-warning' in self.browser.headers)
+        # make sure we are actually testing something
+        self.unwrapped_browser.open(url)
+        self.assertTrue('x-powered-by' in self.unwrapped_browser.headers)
+        self.assertTrue('x-content-type-warning' in self.unwrapped_browser.headers)
+    
+    def test_authorization(self):
+        # Basic authorization headers are encoded in base64
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/echo_one.html?var=HTTP_AUTHORIZATION')
+        self.assertEquals(self.browser.contents, repr('Basic bWdyOm1ncnB3'))
+
+    def test_authorization_other(self):
+        # Non-Basic authorization headers are unmolested
+        self.browser.addHeader('Authorization', 'Digest foobar')
+        self.browser.open('http://localhost/echo_one.html?var=HTTP_AUTHORIZATION')
+        self.assertEquals(self.browser.contents, repr('Digest foobar'))

Deleted: zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py	2011-03-14 01:49:49 UTC (rev 120899)
+++ zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -1,129 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2010-2011 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-import base64
-import re
-import wsgi_intercept
-import wsgi_intercept.mechanize_intercept
-import zope.testbrowser.browser
-
-
-# List of hostname where the test browser/http function replies to
-TEST_HOSTS = ['localhost', '127.0.0.1']
-
-
-class InterceptBrowser(wsgi_intercept.mechanize_intercept.Browser):
-
-    default_schemes = ['http']
-    default_others = ['_http_error',
-                      '_http_default_error']
-    default_features = ['_redirect', '_cookies', '_referer', '_refresh',
-                        '_equiv', '_basicauth', '_digestauth']
-
-
-class Browser(zope.testbrowser.browser.Browser):
-    """Override the zope.testbrowser.browser.Browser interface so that it
-    uses InterceptBrowser.
-    """
-
-    def __init__(self, *args, **kw):
-        kw['mech_browser'] = InterceptBrowser()
-        super(Browser, self).__init__(*args, **kw)
-
-
-# Compatibility helpers to behave like zope.app.testing
-
-basicre = re.compile('Basic (.+)?:(.+)?$')
-
-
-def auth_header(header):
-    """This function takes an authorization HTTP header and encode the
-    couple user, password into base 64 like the HTTP protocol wants
-    it.
-    """
-    match = basicre.match(header)
-    if match:
-        u, p = match.group(1, 2)
-        if u is None:
-            u = ''
-        if p is None:
-            p = ''
-        auth = base64.encodestring('%s:%s' % (u, p))
-        return 'Basic %s' % auth[:-1]
-    return header
-
-
-def is_wanted_header(header):
-    """Return True if the given HTTP header key is wanted.
-    """
-    key, value = header
-    return key.lower() not in ('x-content-type-warning', 'x-powered-by')
-
-
-class AuthorizationMiddleware(object):
-    """This middleware makes the WSGI application compatible with the
-    HTTPCaller behavior defined in zope.app.testing.functional:
-    - It modifies the HTTP Authorization header to encode user and
-      password into base64 if it is Basic authentication.
-    """
-
-    def __init__(self, wsgi_stack):
-        self.wsgi_stack = wsgi_stack
-
-    def __call__(self, environ, start_response):
-        # Handle authorization
-        auth_key = 'HTTP_AUTHORIZATION'
-        if auth_key in environ:
-            environ[auth_key] = auth_header(environ[auth_key])
-
-        # Remove unwanted headers
-        def application_start_response(status, headers, exc_info=None):
-            headers = filter(is_wanted_header, headers)
-            start_response(status, headers)
-
-        for entry in self.wsgi_stack(environ, application_start_response):
-            yield entry
-
-
-class Layer(object):
-    """Test layer which sets up WSGI application for use with
-    wsgi_intercept/testbrowser.
-
-    """
-
-    __bases__ = ()
-    __name__ = 'Layer'
-
-    def make_wsgi_app(self):
-        # Override this method in subclasses of this layer in order to set up
-        # the WSGI application.
-        raise NotImplementedError
-
-    def cooperative_super(self, method_name):
-        # Calling `super` for multiple inheritance:
-        method = getattr(super(Layer, self), method_name, None)
-        if method is not None:
-            method()
-
-    def setUp(self):
-        self.cooperative_super('setUp')
-        self.app = self.make_wsgi_app()
-        factory = lambda: AuthorizationMiddleware(self.app)
-
-        for host in TEST_HOSTS:
-            wsgi_intercept.add_wsgi_intercept(host, 80, factory)
-
-    def tearDown(self):
-        for host in TEST_HOSTS:
-            wsgi_intercept.remove_wsgi_intercept(host, 80)
-        self.cooperative_super('tearDown')

Copied: zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py (from rev 120899, zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/wsgi.py)
===================================================================
--- zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py	                        (rev 0)
+++ zope.testbrowser/trunk/src/zope/testbrowser/wsgi.py	2011-03-14 08:52:34 UTC (rev 120900)
@@ -0,0 +1,258 @@
+##############################################################################
+#
+# Copyright (c) 2010-2011 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""WSGI-specific testing code
+"""
+
+import base64
+import re
+import sys
+
+from webtest import TestApp
+
+import zope.testbrowser.browser
+import zope.testbrowser.connection
+
+class HostNotAllowed(Exception):
+    pass
+
+_allowed_2nd_level = set(['example.com', 'example.net', 'example.org']) # RFC 2606
+
+_allowed = set(['localhost', '127.0.0.1'])
+_allowed.update(_allowed_2nd_level)
+
+class WSGIConnection(object):
+    """A ``mechanize`` compatible connection object."""
+
+    _allowed = True
+
+    def __init__(self, test_app, host, timeout=None):
+        self._test_app = TestApp(test_app)
+        self.host = host
+        self.assert_allowed_host()
+
+    def assert_allowed_host(self):
+        host = self.host
+        if host in _allowed:
+            return
+        for dom in _allowed_2nd_level:
+            if host.endswith('.%s' % dom):
+                return
+        self._allowed = False
+
+    def set_debuglevel(self, level):
+        pass
+
+    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 = '/'
+
+        # Extract the handle_error option header
+        if sys.version_info >= (2,5):
+            handle_errors_key = 'X-Zope-Handle-Errors'
+        else:
+            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)
+        extra_environ = {}
+        if not handle_errors:
+            # There doesn't seem to be a "Right Way" to do this
+            extra_environ['wsgi.handleErrors'] = False # zope.app.wsgi does this
+            extra_environ['paste.throw_errors'] = True # the paste way of doing this
+
+        scheme_key = 'X-Zope-Scheme'
+        extra_environ['wsgi.url_scheme'] = headers.get(scheme_key, 'http')
+        if scheme_key in headers:
+            del headers[scheme_key]
+
+        if not self._allowed:
+            raise HostNotAllowed('%s://%s%s' % (extra_environ['wsgi.url_scheme'], self.host, url))
+
+        app = self._test_app
+
+        # clear our app cookies so that our testbrowser cookie headers don't
+        # get stomped
+        app.cookies.clear()
+
+        # pass the request to webtest
+        if method == 'GET':
+            assert not body, body
+            response = app.get(url, headers=headers, expect_errors=True, extra_environ=extra_environ)
+        elif method == 'POST':
+            response = app.post(url, body, headers=headers, expect_errors=True, extra_environ=extra_environ)
+        else:
+            raise Exception('Couldnt handle method %s' % method)
+
+        self.response = response
+
+    def getresponse(self):
+        """Return a ``mechanize`` compatible response.
+
+        The goal of ths method is to convert the WebTest's reseponse to
+        a ``mechanize`` compatible response, which is also understood by
+        mechanize.
+        """
+        response = self.response
+        status = int(response.status[:3])
+        reason = response.status[4:]
+
+        headers = response.headers.items()
+        headers.sort()
+        headers.insert(0, ('Status', response.status))
+        headers = '\r\n'.join('%s: %s' % h for h in headers)
+        # Ugh! WebTest's headers can at times be unicode. That causes weird
+        # problems later when they are shoved into a StringIO. So just cast
+        # to a string for now using ascii.
+        headers = str(headers)
+        content = response.body
+        return zope.testbrowser.connection.Response(content, headers, status, reason)
+
+
+class WSGIHTTPHandler(zope.testbrowser.connection.HTTPHandler):
+
+    def __init__(self, test_app, *args, **kw):
+        self._test_app = test_app
+        zope.testbrowser.connection.HTTPHandler.__init__(self, *args, **kw)
+
+    def _connect(self, *args, **kw):
+        return WSGIConnection(self._test_app, *args, **kw)
+
+    def https_request(self, req):
+        req.add_unredirected_header('X-Zope-Scheme', 'https')
+        return self.http_request(req)
+
+
+class WSGIMechanizeBrowser(zope.testbrowser.connection.MechanizeBrowser):
+    """Special ``mechanize`` browser using the WSGI HTTP handler."""
+
+    def __init__(self, test_app, *args, **kw):
+        self._test_app = test_app
+        zope.testbrowser.connection.MechanizeBrowser.__init__(self, *args, **kw)
+
+    def _http_handler(self, *args, **kw):
+        return WSGIHTTPHandler(self._test_app, *args, **kw)
+
+
+class Browser(zope.testbrowser.browser.Browser):
+    """A WSGI `testbrowser` Browser that uses a WebTest wrapped WSGI app."""
+
+    def __init__(self, url=None, wsgi_app=None):
+        if wsgi_app is None:
+            wsgi_app = Layer.get_app()
+        if wsgi_app is None:
+            raise AssertionError("wsgi_app not provided or zope.testbrowser.wsgi.Layer not setup")
+        mech_browser = WSGIMechanizeBrowser(wsgi_app)
+        super(Browser, self).__init__(url=url, mech_browser=mech_browser)
+
+# Compatibility helpers to behave like zope.app.testing
+
+basicre = re.compile('Basic (.+)?:(.+)?$')
+
+
+def auth_header(header):
+    """This function takes an authorization HTTP header and encode the
+    couple user, password into base 64 like the HTTP protocol wants
+    it.
+    """
+    match = basicre.match(header)
+    if match:
+        u, p = match.group(1, 2)
+        if u is None:
+            u = ''
+        if p is None:
+            p = ''
+        auth = base64.encodestring('%s:%s' % (u, p))
+        return 'Basic %s' % auth[:-1]
+    return header
+
+
+def is_wanted_header(header):
+    """Return True if the given HTTP header key is wanted.
+    """
+    key, value = header
+    return key.lower() not in ('x-content-type-warning', 'x-powered-by')
+
+
+class AuthorizationMiddleware(object):
+    """This middleware makes the WSGI application compatible with the
+    HTTPCaller behavior defined in zope.app.testing.functional:
+    - It modifies the HTTP Authorization header to encode user and
+      password into base64 if it is Basic authentication.
+    """
+
+    def __init__(self, wsgi_stack):
+        self.wsgi_stack = wsgi_stack
+
+    def __call__(self, environ, start_response):
+        # Handle authorization
+        auth_key = 'HTTP_AUTHORIZATION'
+        if auth_key in environ:
+            environ[auth_key] = auth_header(environ[auth_key])
+
+        # Remove unwanted headers
+        def application_start_response(status, headers, exc_info=None):
+            headers = filter(is_wanted_header, headers)
+            start_response(status, headers)
+
+        for entry in self.wsgi_stack(environ, application_start_response):
+            yield entry
+
+
+_APP_UNDER_TEST = None # setup and torn down by the Layer class
+
+class Layer(object):
+    """Test layer which sets up WSGI application for use with
+    WebTest/testbrowser.
+
+    """
+
+    __bases__ = ()
+    __name__ = 'Layer'
+
+    @classmethod
+    def get_app(cls):
+        return _APP_UNDER_TEST
+
+    def make_wsgi_app(self):
+        # Override this method in subclasses of this layer in order to set up
+        # the WSGI application.
+        raise NotImplementedError
+
+    def cooperative_super(self, method_name):
+        # Calling `super` for multiple inheritance:
+        method = getattr(super(Layer, self), method_name, None)
+        if method is not None:
+            method()
+
+    def setUp(self):
+        self.cooperative_super('setUp')
+        global _APP_UNDER_TEST
+        if _APP_UNDER_TEST is not None:
+            raise AssertionError("Already Setup")
+        _APP_UNDER_TEST = self.make_wsgi_app()
+
+    def tearDown(self):
+        global _APP_UNDER_TEST
+        _APP_UNDER_TEST = None
+        self.cooperative_super('tearDown')



More information about the checkins mailing list