diff --git a/src/allmydata/test/web/test_web.py b/src/allmydata/test/web/test_web.py index 730845a25..1961f99ed 100644 --- a/src/allmydata/test/web/test_web.py +++ b/src/allmydata/test/web/test_web.py @@ -5,12 +5,24 @@ import treq from twisted.application import service from twisted.trial import unittest 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.web import client, error, http 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.loaders import stan +from nevow.testutil import FakeRequest +from nevow.appserver import ( + processingFailed, + DefaultExceptionHandler, +) from allmydata import interfaces, uri, webish 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.nodemaker import NodeMaker from allmydata.web import status +from allmydata.web.common import WebError, MultiFormatPage from allmydata.util import fileutil, base32, hashutil from allmydata.util.consumer import download_to_data 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.mutable import servermap, publish, retrieve 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 .common import unknown_rwcap, unknown_rocap, unknown_immcap, FAVICON_MARKUP # 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'" % (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( + "Exception", + self.render(resource, t=["foo"]), + ) + self.flushLoggedErrors(WebError) + + + class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase): def test_create(self): pass diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py index 335d1de0c..e107f2e0f 100644 --- a/src/allmydata/web/common.py +++ b/src/allmydata/web/common.py @@ -7,6 +7,7 @@ from twisted.python import log from twisted.python.failure import Failure from zope.interface import Interface from nevow import loaders, appserver +from nevow.rend import Page from nevow.inevow import IRequest from nevow.util import resource_filename from allmydata import blacklist @@ -387,6 +388,64 @@ class RenderMixin: 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): """ I provide a rend.Page implementation that only accepts POST calls,