# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Tests for L{twisted.web.template} """ from io import StringIO from typing import List, Optional from zope.interface import implementer from zope.interface.verify import verifyObject from twisted.internet.defer import Deferred, succeed from twisted.logger import globalLogPublisher from twisted.python.failure import Failure from twisted.python.filepath import FilePath from twisted.test.proto_helpers import EventLoggingObserver from twisted.trial.unittest import TestCase from twisted.trial.util import suppress as SUPPRESS from twisted.web._element import UnexposedMethodError from twisted.web.error import FlattenerError, MissingRenderMethod, MissingTemplateLoader from twisted.web.iweb import IRequest, ITemplateLoader from twisted.web.server import NOT_DONE_YET from twisted.web.template import ( Element, Flattenable, Tag, TagLoader, XMLFile, XMLString, renderElement, renderer, tags, ) from twisted.web.test._util import FlattenTestCase from twisted.web.test.test_web import DummyRequest _xmlFileSuppress = SUPPRESS( category=DeprecationWarning, message="Passing filenames or file objects to XMLFile is " "deprecated since Twisted 12.1. Pass a FilePath instead.", ) class TagFactoryTests(TestCase): """ Tests for L{_TagFactory} through the publicly-exposed L{tags} object. """ def test_lookupTag(self) -> None: """ HTML tags can be retrieved through C{tags}. """ tag = tags.a self.assertEqual(tag.tagName, "a") def test_lookupHTML5Tag(self) -> None: """ Twisted supports the latest and greatest HTML tags from the HTML5 specification. """ tag = tags.video self.assertEqual(tag.tagName, "video") def test_lookupTransparentTag(self) -> None: """ To support transparent inclusion in templates, there is a special tag, the transparent tag, which has no name of its own but is accessed through the "transparent" attribute. """ tag = tags.transparent self.assertEqual(tag.tagName, "") def test_lookupInvalidTag(self) -> None: """ Invalid tags which are not part of HTML cause AttributeErrors when accessed through C{tags}. """ self.assertRaises(AttributeError, getattr, tags, "invalid") def test_lookupXMP(self) -> None: """ As a special case, the
Hello, world.
" """ Simple template to use to exercise the loaders. """ def loaderFactory(self) -> ITemplateLoader: raise NotImplementedError def test_load(self) -> None: """ Verify that the loader returns a tag with the correct children. """ assert isinstance(self, TestCase) loader = self.loaderFactory() (tag,) = loader.load() assert isinstance(tag, Tag) warnings = self.flushWarnings(offendingFunctions=[self.loaderFactory]) if self.deprecatedUse: self.assertEqual(len(warnings), 1) self.assertEqual(warnings[0]["category"], DeprecationWarning) self.assertEqual( warnings[0]["message"], "Passing filenames or file objects to XMLFile is " "deprecated since Twisted 12.1. Pass a FilePath instead.", ) else: self.assertEqual(len(warnings), 0) self.assertEqual(tag.tagName, "p") self.assertEqual(tag.children, ["Hello, world."]) def test_loadTwice(self) -> None: """ If {load()} can be called on a loader twice the result should be the same. """ assert isinstance(self, TestCase) loader = self.loaderFactory() tags1 = loader.load() tags2 = loader.load() self.assertEqual(tags1, tags2) test_loadTwice.suppress = [_xmlFileSuppress] # type: ignore[attr-defined] class XMLStringLoaderTests(TestCase, XMLLoaderTestsMixin): """ Tests for L{twisted.web.template.XMLString} """ deprecatedUse = False def loaderFactory(self) -> ITemplateLoader: """ @return: an L{XMLString} constructed with C{self.templateString}. """ return XMLString(self.templateString) class XMLFileWithFilePathTests(TestCase, XMLLoaderTestsMixin): """ Tests for L{twisted.web.template.XMLFile}'s L{FilePath} support. """ deprecatedUse = False def loaderFactory(self) -> ITemplateLoader: """ @return: an L{XMLString} constructed with a L{FilePath} pointing to a file that contains C{self.templateString}. """ fp = FilePath(self.mktemp()) fp.setContent(self.templateString.encode("utf8")) return XMLFile(fp) class XMLFileWithFileTests(TestCase, XMLLoaderTestsMixin): """ Tests for L{twisted.web.template.XMLFile}'s deprecated file object support. """ deprecatedUse = True def loaderFactory(self) -> ITemplateLoader: """ @return: an L{XMLString} constructed with a file object that contains C{self.templateString}. """ return XMLFile(StringIO(self.templateString)) # type: ignore[arg-type] class XMLFileWithFilenameTests(TestCase, XMLLoaderTestsMixin): """ Tests for L{twisted.web.template.XMLFile}'s deprecated filename support. """ deprecatedUse = True def loaderFactory(self) -> ITemplateLoader: """ @return: an L{XMLString} constructed with a filename that points to a file containing C{self.templateString}. """ fp = FilePath(self.mktemp()) fp.setContent(self.templateString.encode("utf8")) return XMLFile(fp.path) # type: ignore[arg-type] class FlattenIntegrationTests(FlattenTestCase): """ Tests for integration between L{Element} and L{twisted.web._flatten.flatten}. """ def test_roundTrip(self) -> None: """ Given a series of parsable XML strings, verify that L{twisted.web._flatten.flatten} will flatten the L{Element} back to the input when sent on a round trip. """ fragments = [ b"Hello, world.
", b"", b"", b'\xe2\x98\x83
", ] for xml in fragments: self.assertFlattensImmediately(Element(loader=XMLString(xml)), xml) def test_entityConversion(self) -> None: """ When flattening an HTML entity, it should flatten out to the utf-8 representation if possible. """ element = Element(loader=XMLString("☃
")) self.assertFlattensImmediately(element, b"\xe2\x98\x83
") def test_missingTemplateLoader(self) -> None: """ Rendering an Element without a loader attribute raises the appropriate exception. """ self.assertFlatteningRaises(Element(), MissingTemplateLoader) def test_missingRenderMethod(self) -> None: """ Flattening an L{Element} with a C{loader} which has a tag with a render directive fails with L{FlattenerError} if there is no available render method to satisfy that directive. """ element = Element( loader=XMLString( """ """ ) ) self.assertFlatteningRaises(element, MissingRenderMethod) def test_transparentRendering(self) -> None: """ A C{transparent} element should be eliminated from the DOM and rendered as only its children. """ element = Element( loader=XMLString( "Goodbye, world.
""" ) ) self.assertFlattensImmediately(element, b"Hello, world.") def test_loaderClassAttribute(self) -> None: """ If there is a non-None loader attribute on the class of an Element instance but none on the instance itself, the class attribute is used. """ class SubElement(Element): loader = XMLString("Hello, world.
") self.assertFlattensImmediately(SubElement(), b"Hello, world.
") def test_directiveRendering(self) -> None: """ An Element with a valid render directive has that directive invoked and the result added to the output. """ renders = [] class RenderfulElement(Element): @renderer def renderMethod( self, request: Optional[IRequest], tag: Tag ) -> Flattenable: renders.append((self, request)) return tag("Hello, world.") element = RenderfulElement( loader=XMLString( """ """ ) ) self.assertFlattensImmediately(element, b"Hello, world.
") def test_directiveRenderingOmittingTag(self) -> None: """ An Element with a render method which omits the containing tag successfully removes that tag from the output. """ class RenderfulElement(Element): @renderer def renderMethod( self, request: Optional[IRequest], tag: Tag ) -> Flattenable: return "Hello, world." element = RenderfulElement( loader=XMLString( """Goodbye, world.
""" ) ) self.assertFlattensImmediately(element, b"Hello, world.") def test_elementContainingStaticElement(self) -> None: """ An Element which is returned by the render method of another Element is rendered properly. """ class RenderfulElement(Element): @renderer def renderMethod( self, request: Optional[IRequest], tag: Tag ) -> Flattenable: return tag(Element(loader=XMLString("Hello, world."))) element = RenderfulElement( loader=XMLString( """ """ ) ) self.assertFlattensImmediately(element, b"Hello, world.
") def test_elementUsingSlots(self) -> None: """ An Element which is returned by the render method of another Element is rendered properly. """ class RenderfulElement(Element): @renderer def renderMethod( self, request: Optional[IRequest], tag: Tag ) -> Flattenable: return tag.fillSlots(test2="world.") element = RenderfulElement( loader=XMLString( ''
'
Hello, world.
") def test_elementContainingDynamicElement(self) -> None: """ Directives in the document factory of an Element returned from a render method of another Element are satisfied from the correct object: the "inner" Element. """ class OuterElement(Element): @renderer def outerMethod(self, request: Optional[IRequest], tag: Tag) -> Flattenable: return tag( InnerElement( loader=XMLString( """Hello, world.
") def test_sameLoaderTwice(self) -> None: """ Rendering the output of a loader, or even the same element, should return different output each time. """ sharedLoader = XMLString( ''
'
1 1
") self.assertFlattensImmediately(e1, b"2 2
") self.assertFlattensImmediately(e2, b"3 1
") class TagLoaderTests(FlattenTestCase): """ Tests for L{TagLoader}. """ def setUp(self) -> None: self.loader = TagLoader(tags.i("test")) def test_interface(self) -> None: """ An instance of L{TagLoader} provides L{ITemplateLoader}. """ self.assertTrue(verifyObject(ITemplateLoader, self.loader)) def test_loadsList(self) -> None: """ L{TagLoader.load} returns a list, per L{ITemplateLoader}. """ self.assertIsInstance(self.loader.load(), list) def test_flatten(self) -> None: """ L{TagLoader} can be used in an L{Element}, and flattens as the tag used to construct the L{TagLoader} would flatten. """ e = Element(self.loader) self.assertFlattensImmediately(e, b"test") class TestElement(Element): """ An L{Element} that can be rendered successfully. """ loader = XMLString( '' "Hello, world." "
" ) class TestFailureElement(Element): """ An L{Element} that can be used in place of L{FailureElement} to verify that L{renderElement} can render failures properly. """ loader = XMLString( '' "I failed." "
" ) def __init__(self, failure: Failure, loader: object = None): self.failure = failure class FailingElement(Element): """ An element that raises an exception when rendered. """ def render(self, request: Optional[IRequest]) -> "Flattenable": a = 42 b = 0 return f"{a // b}" class FakeSite: """ A minimal L{Site} object that we can use to test displayTracebacks """ displayTracebacks = False @implementer(IRequest) class DummyRenderRequest(DummyRequest): # type: ignore[misc] """ A dummy request object that has a C{site} attribute. This does not implement the full IRequest interface, but enough of it for this test suite. """ def __init__(self) -> None: super().__init__([""]) self.site = FakeSite() class RenderElementTests(TestCase): """ Test L{renderElement} """ def setUp(self) -> None: """ Set up a common L{DummyRenderRequest}. """ self.request = DummyRenderRequest() def test_simpleRender(self) -> Deferred[None]: """ L{renderElement} returns NOT_DONE_YET and eventually writes the rendered L{Element} to the request before finishing the request. """ element = TestElement() d = self.request.notifyFinish() def check(_: object) -> None: self.assertEqual( b"".join(self.request.written), b"\n" b"Hello, world.
", ) self.assertTrue(self.request.finished) d.addCallback(check) self.assertIdentical(NOT_DONE_YET, renderElement(self.request, element)) return d def test_simpleFailure(self) -> Deferred[None]: """ L{renderElement} handles failures by writing a minimal error message to the request and finishing it. """ element = FailingElement() d = self.request.notifyFinish() def check(_: object) -> None: flushed = self.flushLoggedErrors(FlattenerError) self.assertEqual(len(flushed), 1) self.assertEqual( b"".join(self.request.written), ( b"\n" b'I failed.
" ) self.assertTrue(self.request.finished) d.addCallback(check) renderElement(self.request, element, _failElement=TestFailureElement) return d def test_nonDefaultDoctype(self) -> Deferred[None]: """ L{renderElement} will write the doctype string specified by the doctype keyword argument. """ element = TestElement() d = self.request.notifyFinish() def check(_: object) -> None: self.assertEqual( b"".join(self.request.written), ( b'\n' b"Hello, world.
" ), ) d.addCallback(check) renderElement( self.request, element, doctype=( b'' ), ) return d def test_noneDoctype(self) -> Deferred[None]: """ L{renderElement} will not write out a doctype if the doctype keyword argument is L{None}. """ element = TestElement() d = self.request.notifyFinish() def check(_: object) -> None: self.assertEqual(b"".join(self.request.written), b"Hello, world.
") d.addCallback(check) renderElement(self.request, element, doctype=None) return d