Skip to content

Commit

Permalink
Make XMLRPC testing infrastructure reusable:
Browse files Browse the repository at this point in the history
Move it from `.xmlrpc.tests` to `.xmlrpc.testing`.
It now requires the WSGI app to be provided.
Use the `testing` extra from `setup.py` to make use of this testing
infrastructure.
  • Loading branch information
Michael Howitz committed Dec 4, 2019
1 parent 5125d8c commit c2d9163
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 115 deletions.
9 changes: 6 additions & 3 deletions CHANGES.rst
Expand Up @@ -2,10 +2,13 @@
CHANGES
=========

4.1.1 (unreleased)
==================
4.2 (unreleased)
================

- Nothing changed yet.
- Move XMLRPC testing infrastructure from ``.xmlrpc.tests`` to
``.xmlrpc.testing`` and make it reusable by requiring the WSGI app to be
provided. Use the ``testing`` extra from `setup.py` to use this testing
infrastructure.


4.1.0 (2018-10-22)
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Expand Up @@ -20,7 +20,7 @@
"""
from setuptools import setup, find_packages

version = '4.1.1.dev0'
version = '4.2.dev0'

def _read(fname):
with open(fname, 'r') as f:
Expand All @@ -44,6 +44,7 @@ def _read(fname):
'zope.browserpage',
'zope.browserresource',
'zope.container',
'zope.deferredimport',
'zope.formlib',
'zope.login',
'zope.principalannotation',
Expand Down Expand Up @@ -109,6 +110,7 @@ def _read(fname):
],
extras_require={
'test': tests_require,
'testing': 'zope.app.wsgi',
},
tests_require=tests_require,
zip_safe=False,
Expand Down
43 changes: 25 additions & 18 deletions src/zope/app/publisher/xmlrpc/README.rst
Expand Up @@ -5,7 +5,9 @@ XML-RPC views
Let's first establish that our management views are around
so we know that we're running in the right context:
>>> print(http(r"""
>>> from zope.app.publisher.testing import AppPublisherLayer
>>> wsgi_app = AppPublisherLayer.make_wsgi_app()
>>> print(http(wsgi_app, r"""
... GET /++etc++site/@@SelectedManagementView.html HTTP/1.0
... Authorization: Basic bWdyOm1ncnB3
... """))
Expand All @@ -14,7 +16,7 @@ XML-RPC views
Content-Type: text/plain;charset=utf-8
Location: @@registration.html

>>> print(http(r"""
>>> print(http(wsgi_app, r"""
... GET /@@SelectedManagementView.html HTTP/1.0
... Authorization: Basic bWdyOm1ncnB3
... """))
Expand All @@ -23,7 +25,7 @@ XML-RPC views
Content-Type: text/plain;charset=utf-8
Location: .

>>> print(http(r"""
>>> print(http(wsgi_app, r"""
... GET /++etc++site/manage HTTP/1.1
... Authorization: Basic bWdyOm1ncnB3
...
Expand Down Expand Up @@ -68,7 +70,7 @@ Now we'll register it as a view:

Now, we'll add some items to the root folder:

>>> print(http(r"""
>>> print(http(wsgi_app, r"""
... POST /@@contents.html HTTP/1.1
... Authorization: Basic bWdyOm1ncnB3
... Content-Length: 73
Expand All @@ -78,7 +80,7 @@ Now, we'll add some items to the root folder:
HTTP/1.1 303 See Other
...

>>> print(http(r"""
>>> print(http(wsgi_app, r"""
... POST /@@contents.html HTTP/1.1
... Authorization: Basic bWdyOm1ncnB3
... Content-Length: 73
Expand All @@ -90,15 +92,16 @@ Now, we'll add some items to the root folder:

And call our xmlrpc method:

>>> from zope.app.publisher.xmlrpc.tests import ServerProxy
>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/")
>>> from zope.app.publisher.xmlrpc.testing import ServerProxy
>>> proxy = ServerProxy(wsgi_app, "http://mgr:mgrpw@localhost/")
>>> proxy.contents()
['f1', 'f2']

Note that we get an unauthorized error if we don't supply authentication
credentials:

>>> proxy = ServerProxy("http://localhost/", handleErrors=False)
>>> proxy = ServerProxy(
... wsgi_app, "http://localhost/", handleErrors=False)
>>> proxy.contents()
Traceback (most recent call last):
...
Expand Down Expand Up @@ -166,16 +169,18 @@ as as a named view:

Now, when we access the `contents`, we do so through the listing view:

>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/listing/")
>>> proxy = ServerProxy(
... wsgi_app, "http://mgr:mgrpw@localhost/listing/")
>>> proxy.contents()
['f1', 'f2']
>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/")
>>> proxy = ServerProxy(wsgi_app, "http://mgr:mgrpw@localhost/")
>>> proxy.listing.contents()
['f1', 'f2']

as before, we will get an error if we don't supply credentials:

>>> proxy = ServerProxy("http://localhost/listing/", handleErrors=False)
>>> proxy = ServerProxy(
... wsgi_app, "http://localhost/listing/", handleErrors=False)
>>> proxy.contents()
Traceback (most recent call last):
...
Expand Down Expand Up @@ -218,7 +223,7 @@ Now we'll register it as a view:
Then we can issue a remote procedure call with a parameter and get
back, surprise!, the sum:

>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/")
>>> proxy = ServerProxy(wsgi_app, "http://mgr:mgrpw@localhost/")
>>> proxy.add(20, 22)
42

Expand Down Expand Up @@ -264,7 +269,7 @@ Now we'll register it as a view:

Now, when we call it, we get a proper XML-RPC fault:

>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/")
>>> proxy = ServerProxy(wsgi_app, "http://mgr:mgrpw@localhost/")
>>> proxy.your_fault()
Traceback (most recent call last):
xmlrpc.client.Fault: <Fault 42: "It's your fault!">
Expand Down Expand Up @@ -309,7 +314,7 @@ Now we'll register it as a view:

Now, when we call it, we get a DateTime value

>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/")
>>> proxy = ServerProxy(wsgi_app, "http://mgr:mgrpw@localhost/")
>>> proxy.epoch()
<DateTime u'19700101T01:00:01' at ...>

Expand Down Expand Up @@ -354,7 +359,8 @@ deferred to the class that provides the view's implementation:
An unauthenticated user can access the public method, but not the protected
one:

>>> proxy = ServerProxy("http://usr:usrpw@localhost/index", handleErrors=False)
>>> proxy = ServerProxy(
... wsgi_app, "http://usr:usrpw@localhost/index", handleErrors=False)
>>> proxy.public()
'foo'
>>> proxy.protected() # doctest: +NORMALIZE_WHITESPACE
Expand All @@ -363,7 +369,7 @@ one:

As a manager, we can access both:

>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/index")
>>> proxy = ServerProxy(wsgi_app, "http://mgr:mgrpw@localhost/index")
>>> proxy.public()
'foo'
>>> proxy.protected()
Expand Down Expand Up @@ -407,15 +413,16 @@ Now we'll register it as a view:

Now, when we call it, we get an XML-RPC fault:

>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/")
>>> proxy = ServerProxy(wsgi_app, "http://mgr:mgrpw@localhost/")
>>> proxy.your_exception()
Traceback (most recent call last):
xmlrpc.client.Fault: <Fault -1: 'Unexpected Zope exception: Exception: Something went wrong!'>

We can also give the parameter `handleErrors` to have the errors not be
handled:

>>> proxy = ServerProxy("http://mgr:mgrpw@localhost/", handleErrors=False)
>>> proxy = ServerProxy(
... wsgi_app, "http://mgr:mgrpw@localhost/", handleErrors=False)
>>> proxy.your_exception()
Traceback (most recent call last):
Exception: Something went wrong!
96 changes: 96 additions & 0 deletions src/zope/app/publisher/xmlrpc/testing.py
@@ -0,0 +1,96 @@
try:
import xmlrpclib
except ImportError:
import xmlrpc.client as xmlrpclib

try:
import httplib
except ImportError:
import http.client as httplib

from io import BytesIO

from zope.app.publisher.testing import AppPublisherLayer
from zope.app.wsgi.testlayer import http as _http


class FakeSocket(object):

def __init__(self, data):
self.data = data

def makefile(self, mode, bufsize=None):
assert 'b' in mode
data = self.data
if not isinstance(data, bytes):
data = data.encode('iso-8859-1')
return BytesIO(data)


def http(wsgi_app, query_str, *args, **kwargs):
wsgi_app = AppPublisherLayer.make_wsgi_app()
# Strip leading \n
query_str = query_str.lstrip()
kwargs.setdefault('handle_errors', True)
if not isinstance(query_str, bytes):
query_str = query_str.encode("utf-8")
return _http(wsgi_app, query_str, *args, **kwargs)


class ZopeTestTransport(xmlrpclib.Transport):
"""xmlrpclib transport that delegates to
zope.app.wsgi.testlayer.http
It can be used like a normal transport, including support for basic
authentication.
"""

verbose = False
handleErrors = True

def __init__(self, wsgi_app):
super(ZopeTestTransport, self).__init__()
self.wsgi_app = wsgi_app

def request(self, host, handler, request_body, verbose=0):
request = "POST %s HTTP/1.0\n" % (handler,)
request += "Content-Length: %i\n" % len(request_body)
request += "Content-Type: text/xml\n"

host, extra_headers, _x509 = self.get_host_info(host)
if extra_headers:
request += "Authorization: %s\n" % (
dict(extra_headers)["Authorization"],)

request += "\n"
if isinstance(request_body, bytes) and str is not bytes:
# Python 3
request = request.encode("ascii")
request += request_body
response = http(
self.wsgi_app, request, handle_errors=self.handleErrors)

errcode = response.getStatus()
errmsg = response.getStatusString()
assert errcode == 200

body = response.getBody()
if not isinstance(body, str):
# Python 3
body = body.decode("utf-8")
content = 'HTTP/1.0 ' + errmsg + '\n\n' + body

res = httplib.HTTPResponse(FakeSocket(content))
res.begin()
return self.parse_response(res)


def ServerProxy(wsgi_app, uri, transport=None, encoding=None,
verbose=0, allow_none=0, handleErrors=True):
"""A factory that creates a server proxy using the ZopeTestTransport
by default.
"""
if transport is None:
transport = ZopeTestTransport(wsgi_app)
if isinstance(transport, ZopeTestTransport):
transport.handleErrors = handleErrors
return xmlrpclib.ServerProxy(uri, transport, encoding, verbose, allow_none)
103 changes: 11 additions & 92 deletions src/zope/app/publisher/xmlrpc/tests/__init__.py
@@ -1,92 +1,11 @@
try:
import xmlrpclib
except ImportError:
import xmlrpc.client as xmlrpclib

try:
import httplib
except ImportError:
import http.client as httplib

from io import BytesIO

from zope.app.publisher.testing import AppPublisherLayer
from zope.app.wsgi.testlayer import http as _http

class FakeSocket(object):

def __init__(self, data):
self.data = data

def makefile(self, mode, bufsize=None):
assert 'b' in mode
data = self.data
if not isinstance(data, bytes):
data = data.encode('iso-8859-1')
return BytesIO(data)

def http(query_str, *args, **kwargs):
wsgi_app = AppPublisherLayer.make_wsgi_app()
# Strip leading \n
query_str = query_str.lstrip()
kwargs.setdefault('handle_errors', True)
if not isinstance(query_str, bytes):
query_str = query_str.encode("utf-8")
return _http(wsgi_app, query_str, *args, **kwargs)


class ZopeTestTransport(xmlrpclib.Transport):
"""xmlrpclib transport that delegates to
zope.app.wsgi.testlayer.http
It can be used like a normal transport, including support for basic
authentication.
"""

verbose = False
handleErrors = True

def request(self, host, handler, request_body, verbose=0):
request = "POST %s HTTP/1.0\n" % (handler,)
request += "Content-Length: %i\n" % len(request_body)
request += "Content-Type: text/xml\n"

host, extra_headers, _x509 = self.get_host_info(host)
if extra_headers:
request += "Authorization: %s\n" % (
dict(extra_headers)["Authorization"],)

request += "\n"
if isinstance(request_body, bytes) and str is not bytes:
# Python 3
request = request.encode("ascii")
request += request_body
response = http(request, handle_errors=self.handleErrors)

errcode = response.getStatus()
errmsg = response.getStatusString()
# This is not the same way that the normal transport deals with the
# headers.
headers = response.getHeaders()

assert errcode == 200

body = response.getBody()
if not isinstance(body, str):
# Python 3
body = body.decode("utf-8")
content = 'HTTP/1.0 ' + errmsg + '\n\n' + body

res = httplib.HTTPResponse(FakeSocket(content))
res.begin()
return self.parse_response(res)

def ServerProxy(uri, transport=None, encoding=None,
verbose=0, allow_none=0, handleErrors=True):
"""A factory that creates a server proxy using the ZopeTestTransport
by default.
"""
if transport is None:
transport = ZopeTestTransport()
if isinstance(transport, ZopeTestTransport):
transport.handleErrors = handleErrors
return xmlrpclib.ServerProxy(uri, transport, encoding, verbose, allow_none)
from zope.deferredimport import deprecated

deprecated(
"The contents of zope/app/publisher/xmlrpc/tests/__init__.py have been"
" moved to zope/app/publisher/xmlrpc/testing.py for reusability."
" Please import from there.",
FakeSocket='zope.app.publisher.xmlrpc.testing:FakeSocket',
http='zope.app.publisher.xmlrpc.testing:http',
ZopeTestTransport='zope.app.publisher.xmlrpc.testing:ZopeTestTransport',
ServerProxy='zope.app.publisher.xmlrpc.testing:ServerProxy',
)

0 comments on commit c2d9163

Please sign in to comment.