test_flatten.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for the flattening portion of L{twisted.web.template}, implemented in
  5. L{twisted.web._flatten}.
  6. """
  7. import sys
  8. import traceback
  9. from xml.etree.cElementTree import XML
  10. from collections import OrderedDict
  11. from zope.interface import implementer
  12. from twisted.trial.unittest import TestCase
  13. from twisted.test.testutils import XMLAssertionMixin
  14. from twisted.internet.defer import passthru, succeed, gatherResults
  15. from twisted.web.iweb import IRenderable
  16. from twisted.web.error import UnfilledSlot, UnsupportedType, FlattenerError
  17. from twisted.web.template import tags, Tag, Comment, CDATA, CharRef, slot
  18. from twisted.web.template import Element, renderer, TagLoader, flattenString
  19. from twisted.web.test._util import FlattenTestCase
  20. class SerializationTests(FlattenTestCase, XMLAssertionMixin):
  21. """
  22. Tests for flattening various things.
  23. """
  24. def test_nestedTags(self):
  25. """
  26. Test that nested tags flatten correctly.
  27. """
  28. return self.assertFlattensTo(
  29. tags.html(tags.body('42'), hi='there'),
  30. b'<html hi="there"><body>42</body></html>')
  31. def test_serializeString(self):
  32. """
  33. Test that strings will be flattened and escaped correctly.
  34. """
  35. return gatherResults([
  36. self.assertFlattensTo('one', b'one'),
  37. self.assertFlattensTo('<abc&&>123', b'&lt;abc&amp;&amp;&gt;123'),
  38. ])
  39. def test_serializeSelfClosingTags(self):
  40. """
  41. The serialized form of a self-closing tag is C{'<tagName />'}.
  42. """
  43. return self.assertFlattensTo(tags.img(), b'<img />')
  44. def test_serializeAttribute(self):
  45. """
  46. The serialized form of attribute I{a} with value I{b} is C{'a="b"'}.
  47. """
  48. self.assertFlattensImmediately(tags.img(src='foo'),
  49. b'<img src="foo" />')
  50. def test_serializedMultipleAttributes(self):
  51. """
  52. Multiple attributes are separated by a single space in their serialized
  53. form.
  54. """
  55. tag = tags.img()
  56. tag.attributes = OrderedDict([("src", "foo"), ("name", "bar")])
  57. self.assertFlattensImmediately(tag, b'<img src="foo" name="bar" />')
  58. def checkAttributeSanitization(self, wrapData, wrapTag):
  59. """
  60. Common implementation of L{test_serializedAttributeWithSanitization}
  61. and L{test_serializedDeferredAttributeWithSanitization},
  62. L{test_serializedAttributeWithTransparentTag}.
  63. @param wrapData: A 1-argument callable that wraps around the
  64. attribute's value so other tests can customize it.
  65. @param wrapData: callable taking L{bytes} and returning something
  66. flattenable
  67. @param wrapTag: A 1-argument callable that wraps around the outer tag
  68. so other tests can customize it.
  69. @type wrapTag: callable taking L{Tag} and returning L{Tag}.
  70. """
  71. self.assertFlattensImmediately(
  72. wrapTag(tags.img(src=wrapData("<>&\""))),
  73. b'<img src="&lt;&gt;&amp;&quot;" />')
  74. def test_serializedAttributeWithSanitization(self):
  75. """
  76. Attribute values containing C{"<"}, C{">"}, C{"&"}, or C{'"'} have
  77. C{"&lt;"}, C{"&gt;"}, C{"&amp;"}, or C{"&quot;"} substituted for those
  78. bytes in the serialized output.
  79. """
  80. self.checkAttributeSanitization(passthru, passthru)
  81. def test_serializedDeferredAttributeWithSanitization(self):
  82. """
  83. Like L{test_serializedAttributeWithSanitization}, but when the contents
  84. of the attribute are in a L{Deferred
  85. <twisted.internet.defer.Deferred>}.
  86. """
  87. self.checkAttributeSanitization(succeed, passthru)
  88. def test_serializedAttributeWithSlotWithSanitization(self):
  89. """
  90. Like L{test_serializedAttributeWithSanitization} but with a slot.
  91. """
  92. toss = []
  93. self.checkAttributeSanitization(
  94. lambda value: toss.append(value) or slot("stuff"),
  95. lambda tag: tag.fillSlots(stuff=toss.pop())
  96. )
  97. def test_serializedAttributeWithTransparentTag(self):
  98. """
  99. Attribute values which are supplied via the value of a C{t:transparent}
  100. tag have the same substitution rules to them as values supplied
  101. directly.
  102. """
  103. self.checkAttributeSanitization(tags.transparent, passthru)
  104. def test_serializedAttributeWithTransparentTagWithRenderer(self):
  105. """
  106. Like L{test_serializedAttributeWithTransparentTag}, but when the
  107. attribute is rendered by a renderer on an element.
  108. """
  109. class WithRenderer(Element):
  110. def __init__(self, value, loader):
  111. self.value = value
  112. super(WithRenderer, self).__init__(loader)
  113. @renderer
  114. def stuff(self, request, tag):
  115. return self.value
  116. toss = []
  117. self.checkAttributeSanitization(
  118. lambda value: toss.append(value) or
  119. tags.transparent(render="stuff"),
  120. lambda tag: WithRenderer(toss.pop(), TagLoader(tag))
  121. )
  122. def test_serializedAttributeWithRenderable(self):
  123. """
  124. Like L{test_serializedAttributeWithTransparentTag}, but when the
  125. attribute is a provider of L{IRenderable} rather than a transparent
  126. tag.
  127. """
  128. @implementer(IRenderable)
  129. class Arbitrary(object):
  130. def __init__(self, value):
  131. self.value = value
  132. def render(self, request):
  133. return self.value
  134. self.checkAttributeSanitization(Arbitrary, passthru)
  135. def checkTagAttributeSerialization(self, wrapTag):
  136. """
  137. Common implementation of L{test_serializedAttributeWithTag} and
  138. L{test_serializedAttributeWithDeferredTag}.
  139. @param wrapTag: A 1-argument callable that wraps around the attribute's
  140. value so other tests can customize it.
  141. @param wrapTag: callable taking L{Tag} and returning something
  142. flattenable
  143. """
  144. innerTag = tags.a('<>&"')
  145. outerTag = tags.img(src=wrapTag(innerTag))
  146. outer = self.assertFlattensImmediately(
  147. outerTag,
  148. b'<img src="&lt;a&gt;&amp;lt;&amp;gt;&amp;amp;&quot;&lt;/a&gt;" />')
  149. inner = self.assertFlattensImmediately(
  150. innerTag, b'<a>&lt;&gt;&amp;"</a>')
  151. # Since the above quoting is somewhat tricky, validate it by making sure
  152. # that the main use-case for tag-within-attribute is supported here: if
  153. # we serialize a tag, it is quoted *such that it can be parsed out again
  154. # as a tag*.
  155. self.assertXMLEqual(XML(outer).attrib['src'], inner)
  156. def test_serializedAttributeWithTag(self):
  157. """
  158. L{Tag} objects which are serialized within the context of an attribute
  159. are serialized such that the text content of the attribute may be
  160. parsed to retrieve the tag.
  161. """
  162. self.checkTagAttributeSerialization(passthru)
  163. def test_serializedAttributeWithDeferredTag(self):
  164. """
  165. Like L{test_serializedAttributeWithTag}, but when the L{Tag} is in a
  166. L{Deferred <twisted.internet.defer.Deferred>}.
  167. """
  168. self.checkTagAttributeSerialization(succeed)
  169. def test_serializedAttributeWithTagWithAttribute(self):
  170. """
  171. Similar to L{test_serializedAttributeWithTag}, but for the additional
  172. complexity where the tag which is the attribute value itself has an
  173. attribute value which contains bytes which require substitution.
  174. """
  175. flattened = self.assertFlattensImmediately(
  176. tags.img(src=tags.a(href='<>&"')),
  177. b'<img src="&lt;a href='
  178. b'&quot;&amp;lt;&amp;gt;&amp;amp;&amp;quot;&quot;&gt;'
  179. b'&lt;/a&gt;" />')
  180. # As in checkTagAttributeSerialization, belt-and-suspenders:
  181. self.assertXMLEqual(XML(flattened).attrib['src'],
  182. b'<a href="&lt;&gt;&amp;&quot;"></a>')
  183. def test_serializeComment(self):
  184. """
  185. Test that comments are correctly flattened and escaped.
  186. """
  187. return self.assertFlattensTo(Comment('foo bar'), b'<!--foo bar-->'),
  188. def test_commentEscaping(self):
  189. """
  190. The data in a L{Comment} is escaped and mangled in the flattened output
  191. so that the result is a legal SGML and XML comment.
  192. SGML comment syntax is complicated and hard to use. This rule is more
  193. restrictive, and more compatible:
  194. Comments start with <!-- and end with --> and never contain -- or >.
  195. Also by XML syntax, a comment may not end with '-'.
  196. @see: U{http://www.w3.org/TR/REC-xml/#sec-comments}
  197. """
  198. def verifyComment(c):
  199. self.assertTrue(
  200. c.startswith(b'<!--'),
  201. "%r does not start with the comment prefix" % (c,))
  202. self.assertTrue(
  203. c.endswith(b'-->'),
  204. "%r does not end with the comment suffix" % (c,))
  205. # If it is shorter than 7, then the prefix and suffix overlap
  206. # illegally.
  207. self.assertTrue(
  208. len(c) >= 7,
  209. "%r is too short to be a legal comment" % (c,))
  210. content = c[4:-3]
  211. self.assertNotIn(b'--', content)
  212. self.assertNotIn(b'>', content)
  213. if content:
  214. self.assertNotEqual(content[-1], b'-')
  215. results = []
  216. for c in [
  217. '',
  218. 'foo---bar',
  219. 'foo---bar-',
  220. 'foo>bar',
  221. 'foo-->bar',
  222. '----------------',
  223. ]:
  224. d = flattenString(None, Comment(c))
  225. d.addCallback(verifyComment)
  226. results.append(d)
  227. return gatherResults(results)
  228. def test_serializeCDATA(self):
  229. """
  230. Test that CDATA is correctly flattened and escaped.
  231. """
  232. return gatherResults([
  233. self.assertFlattensTo(CDATA('foo bar'), b'<![CDATA[foo bar]]>'),
  234. self.assertFlattensTo(
  235. CDATA('foo ]]> bar'),
  236. b'<![CDATA[foo ]]]]><![CDATA[> bar]]>'),
  237. ])
  238. def test_serializeUnicode(self):
  239. """
  240. Test that unicode is encoded correctly in the appropriate places, and
  241. raises an error when it occurs in inappropriate place.
  242. """
  243. snowman = u'\N{SNOWMAN}'
  244. return gatherResults([
  245. self.assertFlattensTo(snowman, b'\xe2\x98\x83'),
  246. self.assertFlattensTo(tags.p(snowman), b'<p>\xe2\x98\x83</p>'),
  247. self.assertFlattensTo(Comment(snowman), b'<!--\xe2\x98\x83-->'),
  248. self.assertFlattensTo(CDATA(snowman), b'<![CDATA[\xe2\x98\x83]]>'),
  249. self.assertFlatteningRaises(
  250. Tag(snowman), UnicodeEncodeError),
  251. self.assertFlatteningRaises(
  252. Tag('p', attributes={snowman: ''}), UnicodeEncodeError),
  253. ])
  254. def test_serializeCharRef(self):
  255. """
  256. A character reference is flattened to a string using the I{&#NNNN;}
  257. syntax.
  258. """
  259. ref = CharRef(ord(u"\N{SNOWMAN}"))
  260. return self.assertFlattensTo(ref, b"&#9731;")
  261. def test_serializeDeferred(self):
  262. """
  263. Test that a deferred is substituted with the current value in the
  264. callback chain when flattened.
  265. """
  266. return self.assertFlattensTo(succeed('two'), b'two')
  267. def test_serializeSameDeferredTwice(self):
  268. """
  269. Test that the same deferred can be flattened twice.
  270. """
  271. d = succeed('three')
  272. return gatherResults([
  273. self.assertFlattensTo(d, b'three'),
  274. self.assertFlattensTo(d, b'three'),
  275. ])
  276. def test_serializeIRenderable(self):
  277. """
  278. Test that flattening respects all of the IRenderable interface.
  279. """
  280. @implementer(IRenderable)
  281. class FakeElement(object):
  282. def render(ign,ored):
  283. return tags.p(
  284. 'hello, ',
  285. tags.transparent(render='test'), ' - ',
  286. tags.transparent(render='test'))
  287. def lookupRenderMethod(ign, name):
  288. self.assertEqual(name, 'test')
  289. return lambda ign, node: node('world')
  290. return gatherResults([
  291. self.assertFlattensTo(FakeElement(), b'<p>hello, world - world</p>'),
  292. ])
  293. def test_serializeSlots(self):
  294. """
  295. Test that flattening a slot will use the slot value from the tag.
  296. """
  297. t1 = tags.p(slot('test'))
  298. t2 = t1.clone()
  299. t2.fillSlots(test='hello, world')
  300. return gatherResults([
  301. self.assertFlatteningRaises(t1, UnfilledSlot),
  302. self.assertFlattensTo(t2, b'<p>hello, world</p>'),
  303. ])
  304. def test_serializeDeferredSlots(self):
  305. """
  306. Test that a slot with a deferred as its value will be flattened using
  307. the value from the deferred.
  308. """
  309. t = tags.p(slot('test'))
  310. t.fillSlots(test=succeed(tags.em('four>')))
  311. return self.assertFlattensTo(t, b'<p><em>four&gt;</em></p>')
  312. def test_unknownTypeRaises(self):
  313. """
  314. Test that flattening an unknown type of thing raises an exception.
  315. """
  316. return self.assertFlatteningRaises(None, UnsupportedType)
  317. # Use the co_filename mechanism (instead of the __file__ mechanism) because
  318. # it is the mechanism traceback formatting uses. The two do not necessarily
  319. # agree with each other. This requires a code object compiled in this file.
  320. # The easiest way to get a code object is with a new function. I'll use a
  321. # lambda to avoid adding anything else to this namespace. The result will
  322. # be a string which agrees with the one the traceback module will put into a
  323. # traceback for frames associated with functions defined in this file.
  324. HERE = (lambda: None).__code__.co_filename
  325. class FlattenerErrorTests(TestCase):
  326. """
  327. Tests for L{FlattenerError}.
  328. """
  329. def test_renderable(self):
  330. """
  331. If a L{FlattenerError} is created with an L{IRenderable} provider root,
  332. the repr of that object is included in the string representation of the
  333. exception.
  334. """
  335. @implementer(IRenderable)
  336. class Renderable(object):
  337. def __repr__(self):
  338. return "renderable repr"
  339. self.assertEqual(
  340. str(FlattenerError(
  341. RuntimeError("reason"), [Renderable()], [])),
  342. "Exception while flattening:\n"
  343. " renderable repr\n"
  344. "RuntimeError: reason\n")
  345. def test_tag(self):
  346. """
  347. If a L{FlattenerError} is created with a L{Tag} instance with source
  348. location information, the source location is included in the string
  349. representation of the exception.
  350. """
  351. tag = Tag(
  352. 'div', filename='/foo/filename.xhtml', lineNumber=17, columnNumber=12)
  353. self.assertEqual(
  354. str(FlattenerError(RuntimeError("reason"), [tag], [])),
  355. "Exception while flattening:\n"
  356. " File \"/foo/filename.xhtml\", line 17, column 12, in \"div\"\n"
  357. "RuntimeError: reason\n")
  358. def test_tagWithoutLocation(self):
  359. """
  360. If a L{FlattenerError} is created with a L{Tag} instance without source
  361. location information, only the tagName is included in the string
  362. representation of the exception.
  363. """
  364. self.assertEqual(
  365. str(FlattenerError(RuntimeError("reason"), [Tag('span')], [])),
  366. "Exception while flattening:\n"
  367. " Tag <span>\n"
  368. "RuntimeError: reason\n")
  369. def test_traceback(self):
  370. """
  371. If a L{FlattenerError} is created with traceback frames, they are
  372. included in the string representation of the exception.
  373. """
  374. # Try to be realistic in creating the data passed in for the traceback
  375. # frames.
  376. def f():
  377. g()
  378. def g():
  379. raise RuntimeError("reason")
  380. try:
  381. f()
  382. except RuntimeError as e:
  383. # Get the traceback, minus the info for *this* frame
  384. tbinfo = traceback.extract_tb(sys.exc_info()[2])[1:]
  385. exc = e
  386. else:
  387. self.fail("f() must raise RuntimeError")
  388. self.assertEqual(
  389. str(FlattenerError(exc, [], tbinfo)),
  390. "Exception while flattening:\n"
  391. " File \"%s\", line %d, in f\n"
  392. " g()\n"
  393. " File \"%s\", line %d, in g\n"
  394. " raise RuntimeError(\"reason\")\n"
  395. "RuntimeError: reason\n" % (
  396. HERE, f.__code__.co_firstlineno + 1,
  397. HERE, g.__code__.co_firstlineno + 1))