a helper for the ?t=json resources

This commit is contained in:
Jean-Paul Calderone 2017-07-25 11:11:38 -04:00
parent 7cad9e1b5d
commit 7c8c63a01f
2 changed files with 189 additions and 2 deletions

View File

@ -5,12 +5,24 @@ import treq
from twisted.application import service from twisted.application import service
from twisted.trial import unittest from twisted.trial import unittest
from twisted.internet import defer, reactor from twisted.internet import defer, reactor
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
from twisted.internet.task import Clock from twisted.internet.task import Clock
from twisted.web import client, error, http from twisted.web import client, error, http
from twisted.python import failure, log from twisted.python import failure, log
from nevow.context import WebContext
from nevow.inevow import (
ICanHandleException,
IRequest,
IData,
)
from nevow.util import escapeToXML from nevow.util import escapeToXML
from nevow.loaders import stan
from nevow.testutil import FakeRequest
from nevow.appserver import (
processingFailed,
DefaultExceptionHandler,
)
from allmydata import interfaces, uri, webish from allmydata import interfaces, uri, webish
from allmydata.storage_client import StorageFarmBroker, StubServer from allmydata.storage_client import StorageFarmBroker, StubServer
@ -19,6 +31,7 @@ from allmydata.immutable.downloader.status import DownloadStatus
from allmydata.dirnode import DirectoryNode from allmydata.dirnode import DirectoryNode
from allmydata.nodemaker import NodeMaker from allmydata.nodemaker import NodeMaker
from allmydata.web import status from allmydata.web import status
from allmydata.web.common import WebError, MultiFormatPage
from allmydata.util import fileutil, base32, hashutil from allmydata.util import fileutil, base32, hashutil
from allmydata.util.consumer import download_to_data from allmydata.util.consumer import download_to_data
from allmydata.util.encodingutil import to_str from allmydata.util.encodingutil import to_str
@ -29,7 +42,11 @@ from ..common import FakeCHKFileNode, FakeMutableFileNode, \
from allmydata.interfaces import IMutableFileNode, SDMF_VERSION, MDMF_VERSION from allmydata.interfaces import IMutableFileNode, SDMF_VERSION, MDMF_VERSION
from allmydata.mutable import servermap, publish, retrieve from allmydata.mutable import servermap, publish, retrieve
from .. import common_util as testutil from .. import common_util as testutil
from ..common_web import HTTPClientGETFactory, do_http, Error from ..common_web import (
HTTPClientGETFactory,
do_http,
Error,
)
from allmydata.client import Client, SecretHolder from allmydata.client import Client, SecretHolder
from .common import unknown_rwcap, unknown_rocap, unknown_immcap, FAVICON_MARKUP from .common import unknown_rwcap, unknown_rocap, unknown_immcap, FAVICON_MARKUP
# create a fake uploader/downloader, and a couple of fake dirnodes, then # create a fake uploader/downloader, and a couple of fake dirnodes, then
@ -602,6 +619,117 @@ class WebMixin(testutil.TimezoneMixin):
self.fail("%s was supposed to Error(302), not get '%s'" % self.fail("%s was supposed to Error(302), not get '%s'" %
(which, res)) (which, res))
class MultiFormatPageTests(unittest.TestCase):
"""
Tests for ``MultiFormatPage``.
"""
def resource(self):
"""
Create and return an instance of a ``MultiFormatPage`` subclass with two
formats: ``a`` and ``b``.
"""
class Content(MultiFormatPage):
docFactory = stan("doc factory")
def render_A(self, req):
return "a"
def render_B(self, req):
return "b"
return Content()
def render(self, resource, **query_args):
"""
Render a Nevow ``Page`` against a request with the given query arguments.
:param resource: The Nevow resource to render.
:param query_args: The query arguments to put into the request being
rendered. A mapping from ``bytes`` to ``list`` of ``bytes``.
:return: The rendered response body as ``bytes``.
"""
ctx = WebContext(tag=resource)
req = FakeRequest(args=query_args)
ctx.remember(DefaultExceptionHandler(), ICanHandleException)
ctx.remember(req, IRequest)
ctx.remember(None, IData)
d = maybeDeferred(resource.renderHTTP, ctx)
d.addErrback(processingFailed, req, ctx)
res = self.successResultOf(d)
if isinstance(res, bytes):
return req.v + res
return req.v
def test_select_format(self):
"""
The ``formatArgument`` attribute of a ``MultiFormatPage`` subclass
identifies the query argument which selects the result format.
"""
resource = self.resource()
resource.formatArgument = "foo"
self.assertEqual("a", self.render(resource, foo=["a"]))
def test_default_format_argument(self):
"""
If a ``MultiFormatPage`` subclass does not set ``formatArgument`` then the
``t`` argument is used.
"""
resource = self.resource()
self.assertEqual("a", self.render(resource, t=["a"]))
def test_no_format(self):
"""
If no value is given for the format argument and no default format has
been defined, the base Nevow rendering behavior is used
(``renderHTTP``).
"""
resource = self.resource()
self.assertEqual("doc factory", self.render(resource))
def test_default_format(self):
"""
If no value is given for the format argument and the ``MultiFormatPage``
subclass defines a ``formatDefault``, that value is used as the format
to render.
"""
resource = self.resource()
resource.formatDefault = "b"
self.assertEqual("b", self.render(resource))
def test_explicit_none_format_renderer(self):
"""
If a format is selected which has a renderer set to ``None``, the base
Nevow rendering behavior is used (``renderHTTP``).
"""
resource = self.resource()
resource.render_FOO = None
self.assertEqual("doc factory", self.render(resource, t=["foo"]))
def test_unknown_format(self):
"""
If a format is selected for which there is no renderer, an error is
returned.
"""
resource = self.resource()
self.assertIn(
"<title>Exception</title>",
self.render(resource, t=["foo"]),
)
self.flushLoggedErrors(WebError)
class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase): class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase):
def test_create(self): def test_create(self):
pass pass

View File

@ -7,6 +7,7 @@ from twisted.python import log
from twisted.python.failure import Failure from twisted.python.failure import Failure
from zope.interface import Interface from zope.interface import Interface
from nevow import loaders, appserver from nevow import loaders, appserver
from nevow.rend import Page
from nevow.inevow import IRequest from nevow.inevow import IRequest
from nevow.util import resource_filename from nevow.util import resource_filename
from allmydata import blacklist from allmydata import blacklist
@ -387,6 +388,64 @@ class RenderMixin:
return m(ctx) return m(ctx)
class MultiFormatPage(Page):
"""
```MultiFormatPage`` is a ``rend.Page`` that can be rendered in a number
of different formats.
Rendered format is controlled by a query argument (given by
``self.formatArgument``). Different resources may support different
formats but ``json`` is a pretty common one.
"""
formatArgument = "t"
formatDefault = None
def renderHTTP(self, ctx):
"""
Dispatch to a renderer for a particular format, as selected by a query
argument.
A renderer for the format given by the query argument matching
``formatArgument`` will be selected and invoked. The default ``Page``
rendering behavior will be used if no format is selected (either by
query arguments or by ``formatDefault``).
:return: The result of the selected renderer.
"""
req = IRequest(ctx)
t = get_arg(req, self.formatArgument, self.formatDefault)
renderer = self._get_renderer(t)
result = renderer(ctx)
return result
def _get_renderer(self, fmt):
"""
Get the renderer for the indicated format.
:param bytes fmt: The format. If a method with a prefix of
``render_`` and a suffix of this format (upper-cased) is found, it
will be used.
:return: A callable which takes a Nevow context and renders a
response.
"""
if fmt is None:
return super(MultiFormatPage, self).renderHTTP
try:
renderer = getattr(self, "render_{}".format(fmt.upper()))
except AttributeError:
raise WebError(
"Unknown {} value: {!r}".format(self.formatArgument, fmt),
)
else:
if renderer is None:
return super(MultiFormatPage, self).renderHTTP
return lambda ctx: renderer(IRequest(ctx))
class TokenOnlyWebApi(resource.Resource): class TokenOnlyWebApi(resource.Resource):
""" """
I provide a rend.Page implementation that only accepts POST calls, I provide a rend.Page implementation that only accepts POST calls,