tests.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import os
  4. import sys
  5. import unittest
  6. from django import template
  7. from django.contrib.auth.models import Group
  8. from django.core import urlresolvers
  9. from django.template import (
  10. Context, RequestContext, Template, TemplateSyntaxError,
  11. base as template_base, engines, loader,
  12. )
  13. from django.template.engine import Engine
  14. from django.template.loaders import app_directories, filesystem
  15. from django.test import RequestFactory, SimpleTestCase
  16. from django.test.utils import (
  17. extend_sys_path, ignore_warnings, override_settings,
  18. )
  19. from django.utils._os import upath
  20. from django.utils.deprecation import RemovedInDjango20Warning
  21. TESTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(upath(__file__))))
  22. TEMPLATES_DIR = os.path.join(TESTS_DIR, 'templates')
  23. class TemplateLoaderTests(SimpleTestCase):
  24. def test_loaders_security(self):
  25. ad_loader = app_directories.Loader(Engine.get_default())
  26. fs_loader = filesystem.Loader(Engine.get_default())
  27. def test_template_sources(path, template_dirs, expected_sources):
  28. if isinstance(expected_sources, list):
  29. # Fix expected sources so they are abspathed
  30. expected_sources = [os.path.abspath(s) for s in expected_sources]
  31. # Test the two loaders (app_directores and filesystem).
  32. func1 = lambda p, t: list(ad_loader.get_template_sources(p, t))
  33. func2 = lambda p, t: list(fs_loader.get_template_sources(p, t))
  34. for func in (func1, func2):
  35. if isinstance(expected_sources, list):
  36. self.assertEqual(func(path, template_dirs), expected_sources)
  37. else:
  38. self.assertRaises(expected_sources, func, path, template_dirs)
  39. template_dirs = ['/dir1', '/dir2']
  40. test_template_sources('index.html', template_dirs,
  41. ['/dir1/index.html', '/dir2/index.html'])
  42. test_template_sources('/etc/passwd', template_dirs, [])
  43. test_template_sources('etc/passwd', template_dirs,
  44. ['/dir1/etc/passwd', '/dir2/etc/passwd'])
  45. test_template_sources('../etc/passwd', template_dirs, [])
  46. test_template_sources('../../../etc/passwd', template_dirs, [])
  47. test_template_sources('/dir1/index.html', template_dirs,
  48. ['/dir1/index.html'])
  49. test_template_sources('../dir2/index.html', template_dirs,
  50. ['/dir2/index.html'])
  51. test_template_sources('/dir1blah', template_dirs, [])
  52. test_template_sources('../dir1blah', template_dirs, [])
  53. # UTF-8 bytestrings are permitted.
  54. test_template_sources(b'\xc3\x85ngstr\xc3\xb6m', template_dirs,
  55. ['/dir1/Ångström', '/dir2/Ångström'])
  56. # Unicode strings are permitted.
  57. test_template_sources('Ångström', template_dirs,
  58. ['/dir1/Ångström', '/dir2/Ångström'])
  59. test_template_sources('Ångström', [b'/Stra\xc3\x9fe'], ['/Straße/Ångström'])
  60. test_template_sources(b'\xc3\x85ngstr\xc3\xb6m', [b'/Stra\xc3\x9fe'],
  61. ['/Straße/Ångström'])
  62. # Invalid UTF-8 encoding in bytestrings is not. Should raise a
  63. # semi-useful error message.
  64. test_template_sources(b'\xc3\xc3', template_dirs, UnicodeDecodeError)
  65. # Case insensitive tests (for win32). Not run unless we're on
  66. # a case insensitive operating system.
  67. if os.path.normcase('/TEST') == os.path.normpath('/test'):
  68. template_dirs = ['/dir1', '/DIR2']
  69. test_template_sources('index.html', template_dirs,
  70. ['/dir1/index.html', '/DIR2/index.html'])
  71. test_template_sources('/DIR1/index.HTML', template_dirs,
  72. ['/DIR1/index.HTML'])
  73. @override_settings(TEMPLATES=[{
  74. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  75. 'DIRS': [TEMPLATES_DIR],
  76. 'OPTIONS': {
  77. # Turn DEBUG on, so that the origin file name will be kept with
  78. # the compiled templates.
  79. 'debug': True,
  80. }
  81. }])
  82. def test_loader_debug_origin(self):
  83. load_name = 'login.html'
  84. # We rely on the fact the file system and app directories loaders both
  85. # inherit the load_template method from the base Loader class, so we
  86. # only need to test one of them.
  87. template = loader.get_template(load_name).template
  88. template_name = template.nodelist[0].source[0].name
  89. self.assertTrue(template_name.endswith(load_name),
  90. 'Template loaded by filesystem loader has incorrect name for debug page: %s' % template_name)
  91. @override_settings(TEMPLATES=[{
  92. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  93. 'DIRS': [TEMPLATES_DIR],
  94. 'OPTIONS': {
  95. 'debug': True,
  96. 'loaders': [
  97. ('django.template.loaders.cached.Loader', [
  98. 'django.template.loaders.filesystem.Loader',
  99. ]),
  100. ],
  101. },
  102. }])
  103. def test_cached_loader_debug_origin(self):
  104. load_name = 'login.html'
  105. # Test the cached loader separately since it overrides load_template.
  106. template = loader.get_template(load_name).template
  107. template_name = template.nodelist[0].source[0].name
  108. self.assertTrue(template_name.endswith(load_name),
  109. 'Template loaded through cached loader has incorrect name for debug page: %s' % template_name)
  110. template = loader.get_template(load_name).template
  111. template_name = template.nodelist[0].source[0].name
  112. self.assertTrue(template_name.endswith(load_name),
  113. 'Cached template loaded through cached loader has incorrect name for debug page: %s' % template_name)
  114. @override_settings(DEBUG=True)
  115. def test_loader_origin(self):
  116. template = loader.get_template('login.html')
  117. self.assertEqual(template.origin.loadname, 'login.html')
  118. @override_settings(DEBUG=True)
  119. def test_string_origin(self):
  120. template = Template('string template')
  121. self.assertEqual(template.origin.source, 'string template')
  122. def test_debug_false_origin(self):
  123. template = loader.get_template('login.html')
  124. self.assertEqual(template.origin, None)
  125. # Test the base loader class via the app loader. load_template
  126. # from base is used by all shipped loaders excepting cached,
  127. # which has its own test.
  128. @override_settings(TEMPLATES=[{
  129. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  130. 'APP_DIRS': True,
  131. 'OPTIONS': {
  132. # Enable debug, otherwise the exception raised during
  133. # {% include %} processing will be suppressed.
  134. 'debug': True,
  135. }
  136. }])
  137. def test_include_missing_template(self):
  138. """
  139. Tests that the correct template is identified as not existing
  140. when {% include %} specifies a template that does not exist.
  141. """
  142. load_name = 'test_include_error.html'
  143. r = None
  144. try:
  145. tmpl = loader.select_template([load_name])
  146. r = tmpl.render()
  147. except template.TemplateDoesNotExist as e:
  148. self.assertEqual(e.args[0], 'missing.html')
  149. self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r)
  150. # Test the base loader class via the app loader. load_template
  151. # from base is used by all shipped loaders excepting cached,
  152. # which has its own test.
  153. @override_settings(TEMPLATES=[{
  154. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  155. 'APP_DIRS': True,
  156. 'OPTIONS': {
  157. # Enable debug, otherwise the exception raised during
  158. # {% include %} processing will be suppressed.
  159. 'debug': True,
  160. }
  161. }])
  162. def test_extends_include_missing_baseloader(self):
  163. """
  164. Tests that the correct template is identified as not existing
  165. when {% extends %} specifies a template that does exist, but
  166. that template has an {% include %} of something that does not
  167. exist. See #12787.
  168. """
  169. load_name = 'test_extends_error.html'
  170. tmpl = loader.get_template(load_name)
  171. r = None
  172. try:
  173. r = tmpl.render()
  174. except template.TemplateDoesNotExist as e:
  175. self.assertEqual(e.args[0], 'missing.html')
  176. self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r)
  177. @override_settings(TEMPLATES=[{
  178. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  179. 'OPTIONS': {
  180. 'debug': True,
  181. 'loaders': [
  182. ('django.template.loaders.cached.Loader', [
  183. 'django.template.loaders.app_directories.Loader',
  184. ]),
  185. ],
  186. },
  187. }])
  188. def test_extends_include_missing_cachedloader(self):
  189. """
  190. Same as test_extends_include_missing_baseloader, only tests
  191. behavior of the cached loader instead of base loader.
  192. """
  193. load_name = 'test_extends_error.html'
  194. tmpl = loader.get_template(load_name)
  195. r = None
  196. try:
  197. r = tmpl.render()
  198. except template.TemplateDoesNotExist as e:
  199. self.assertEqual(e.args[0], 'missing.html')
  200. self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r)
  201. # For the cached loader, repeat the test, to ensure the first attempt did not cache a
  202. # result that behaves incorrectly on subsequent attempts.
  203. tmpl = loader.get_template(load_name)
  204. try:
  205. tmpl.render()
  206. except template.TemplateDoesNotExist as e:
  207. self.assertEqual(e.args[0], 'missing.html')
  208. self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r)
  209. def test_include_template_argument(self):
  210. """
  211. Support any render() supporting object
  212. """
  213. ctx = Context({
  214. 'tmpl': Template('This worked!'),
  215. })
  216. outer_tmpl = Template('{% include tmpl %}')
  217. output = outer_tmpl.render(ctx)
  218. self.assertEqual(output, 'This worked!')
  219. @override_settings(TEMPLATES=[{
  220. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  221. 'OPTIONS': {
  222. 'debug': True,
  223. },
  224. }])
  225. def test_include_immediate_missing(self):
  226. """
  227. Test that an {% include %} tag with a literal string referencing a
  228. template that does not exist does not raise an exception at parse
  229. time. Regression test for #16417.
  230. """
  231. tmpl = Template('{% include "this_does_not_exist.html" %}')
  232. self.assertIsInstance(tmpl, Template)
  233. @override_settings(TEMPLATES=[{
  234. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  235. 'APP_DIRS': True,
  236. 'OPTIONS': {
  237. 'debug': True,
  238. },
  239. }])
  240. def test_include_recursive(self):
  241. comments = [
  242. {
  243. 'comment': 'A1',
  244. 'children': [
  245. {'comment': 'B1', 'children': []},
  246. {'comment': 'B2', 'children': []},
  247. {'comment': 'B3', 'children': [
  248. {'comment': 'C1', 'children': []}
  249. ]},
  250. ]
  251. }
  252. ]
  253. t = loader.get_template('recursive_include.html')
  254. self.assertEqual(
  255. "Recursion! A1 Recursion! B1 B2 B3 Recursion! C1",
  256. t.render({'comments': comments}).replace(' ', '').replace('\n', ' ').strip(),
  257. )
  258. class TemplateRegressionTests(SimpleTestCase):
  259. def test_token_smart_split(self):
  260. # Regression test for #7027
  261. token = template_base.Token(template_base.TOKEN_BLOCK, 'sometag _("Page not found") value|yesno:_("yes,no")')
  262. split = token.split_contents()
  263. self.assertEqual(split, ["sometag", '_("Page not found")', 'value|yesno:_("yes,no")'])
  264. @override_settings(SETTINGS_MODULE=None, DEBUG=True)
  265. def test_url_reverse_no_settings_module(self):
  266. # Regression test for #9005
  267. t = Template('{% url will_not_match %}')
  268. c = Context()
  269. with self.assertRaises(urlresolvers.NoReverseMatch):
  270. t.render(c)
  271. @override_settings(
  272. TEMPLATES=[{
  273. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  274. 'OPTIONS': {'string_if_invalid': '%s is invalid'},
  275. }],
  276. SETTINGS_MODULE='also_something',
  277. )
  278. def test_url_reverse_view_name(self):
  279. # Regression test for #19827
  280. t = Template('{% url will_not_match %}')
  281. c = Context()
  282. try:
  283. t.render(c)
  284. except urlresolvers.NoReverseMatch:
  285. tb = sys.exc_info()[2]
  286. depth = 0
  287. while tb.tb_next is not None:
  288. tb = tb.tb_next
  289. depth += 1
  290. self.assertGreater(depth, 5,
  291. "The traceback context was lost when reraising the traceback. See #19827")
  292. @override_settings(DEBUG=True)
  293. def test_no_wrapped_exception(self):
  294. """
  295. The template system doesn't wrap exceptions, but annotates them.
  296. Refs #16770
  297. """
  298. c = Context({"coconuts": lambda: 42 / 0})
  299. t = Template("{{ coconuts }}")
  300. with self.assertRaises(ZeroDivisionError) as cm:
  301. t.render(c)
  302. self.assertEqual(cm.exception.django_template_source[1], (0, 14))
  303. def test_invalid_block_suggestion(self):
  304. # See #7876
  305. try:
  306. Template("{% if 1 %}lala{% endblock %}{% endif %}")
  307. except TemplateSyntaxError as e:
  308. self.assertEqual(e.args[0], "Invalid block tag: 'endblock', expected 'elif', 'else' or 'endif'")
  309. def test_ifchanged_concurrency(self):
  310. # Tests for #15849
  311. template = Template('[0{% for x in foo %},{% with var=get_value %}{% ifchanged %}{{ var }}{% endifchanged %}{% endwith %}{% endfor %}]')
  312. # Using generator to mimic concurrency.
  313. # The generator is not passed to the 'for' loop, because it does a list(values)
  314. # instead, call gen.next() in the template to control the generator.
  315. def gen():
  316. yield 1
  317. yield 2
  318. # Simulate that another thread is now rendering.
  319. # When the IfChangeNode stores state at 'self' it stays at '3' and skip the last yielded value below.
  320. iter2 = iter([1, 2, 3])
  321. output2 = template.render(Context({'foo': range(3), 'get_value': lambda: next(iter2)}))
  322. self.assertEqual(output2, '[0,1,2,3]', 'Expected [0,1,2,3] in second parallel template, got {}'.format(output2))
  323. yield 3
  324. gen1 = gen()
  325. output1 = template.render(Context({'foo': range(3), 'get_value': lambda: next(gen1)}))
  326. self.assertEqual(output1, '[0,1,2,3]', 'Expected [0,1,2,3] in first template, got {}'.format(output1))
  327. def test_cache_regression_20130(self):
  328. t = Template('{% load cache %}{% cache 1 regression_20130 %}foo{% endcache %}')
  329. cachenode = t.nodelist[1]
  330. self.assertEqual(cachenode.fragment_name, 'regression_20130')
  331. @override_settings(CACHES={
  332. 'default': {
  333. 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
  334. 'LOCATION': 'default',
  335. },
  336. 'template_fragments': {
  337. 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
  338. 'LOCATION': 'fragments',
  339. },
  340. })
  341. def test_cache_fragment_cache(self):
  342. """
  343. When a cache called "template_fragments" is present, the cache tag
  344. will use it in preference to 'default'
  345. """
  346. t1 = Template('{% load cache %}{% cache 1 fragment %}foo{% endcache %}')
  347. t2 = Template('{% load cache %}{% cache 1 fragment using="default" %}bar{% endcache %}')
  348. ctx = Context()
  349. o1 = t1.render(ctx)
  350. o2 = t2.render(ctx)
  351. self.assertEqual(o1, 'foo')
  352. self.assertEqual(o2, 'bar')
  353. def test_cache_missing_backend(self):
  354. """
  355. When a cache that doesn't exist is specified, the cache tag will
  356. raise a TemplateSyntaxError
  357. '"""
  358. t = Template('{% load cache %}{% cache 1 backend using="unknown" %}bar{% endcache %}')
  359. ctx = Context()
  360. with self.assertRaises(TemplateSyntaxError):
  361. t.render(ctx)
  362. def test_ifchanged_render_once(self):
  363. """ Test for ticket #19890. The content of ifchanged template tag was
  364. rendered twice."""
  365. template = Template('{% ifchanged %}{% cycle "1st time" "2nd time" %}{% endifchanged %}')
  366. output = template.render(Context({}))
  367. self.assertEqual(output, '1st time')
  368. def test_super_errors(self):
  369. """
  370. Test behavior of the raise errors into included blocks.
  371. See #18169
  372. """
  373. t = loader.get_template('included_content.html')
  374. with self.assertRaises(urlresolvers.NoReverseMatch):
  375. t.render()
  376. def test_debug_tag_non_ascii(self):
  377. """
  378. Test non-ASCII model representation in debug output (#23060).
  379. """
  380. Group.objects.create(name="清風")
  381. c1 = Context({"objs": Group.objects.all()})
  382. t1 = Template('{% debug %}')
  383. self.assertIn("清風", t1.render(c1))
  384. def test_extends_generic_template(self):
  385. """
  386. {% extends %} accepts django.template.backends.django.Template (#24338).
  387. """
  388. parent = engines['django'].from_string(
  389. '{% block content %}parent{% endblock %}')
  390. child = engines['django'].from_string(
  391. '{% extends parent %}{% block content %}child{% endblock %}')
  392. self.assertEqual(child.render({'parent': parent}), 'child')
  393. class TemplateTagLoading(SimpleTestCase):
  394. def setUp(self):
  395. self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__))
  396. def test_load_error(self):
  397. ttext = "{% load broken_tag %}"
  398. self.assertRaises(template.TemplateSyntaxError, template.Template, ttext)
  399. try:
  400. template.Template(ttext)
  401. except template.TemplateSyntaxError as e:
  402. self.assertIn('ImportError', e.args[0])
  403. self.assertIn('Xtemplate', e.args[0])
  404. def test_load_error_egg(self):
  405. ttext = "{% load broken_egg %}"
  406. egg_name = '%s/tagsegg.egg' % self.egg_dir
  407. with extend_sys_path(egg_name):
  408. with self.assertRaises(template.TemplateSyntaxError):
  409. with self.settings(INSTALLED_APPS=['tagsegg']):
  410. template.Template(ttext)
  411. try:
  412. with self.settings(INSTALLED_APPS=['tagsegg']):
  413. template.Template(ttext)
  414. except template.TemplateSyntaxError as e:
  415. self.assertIn('ImportError', e.args[0])
  416. self.assertIn('Xtemplate', e.args[0])
  417. def test_load_working_egg(self):
  418. ttext = "{% load working_egg %}"
  419. egg_name = '%s/tagsegg.egg' % self.egg_dir
  420. with extend_sys_path(egg_name):
  421. with self.settings(INSTALLED_APPS=['tagsegg']):
  422. template.Template(ttext)
  423. class RequestContextTests(unittest.TestCase):
  424. def setUp(self):
  425. self.fake_request = RequestFactory().get('/')
  426. @override_settings(TEMPLATES=[{
  427. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  428. 'OPTIONS': {
  429. 'loaders': [
  430. ('django.template.loaders.locmem.Loader', {
  431. 'child': '{{ var|default:"none" }}',
  432. }),
  433. ],
  434. },
  435. }])
  436. def test_include_only(self):
  437. """
  438. Regression test for #15721, ``{% include %}`` and ``RequestContext``
  439. not playing together nicely.
  440. """
  441. ctx = RequestContext(self.fake_request, {'var': 'parent'})
  442. self.assertEqual(
  443. template.Template('{% include "child" %}').render(ctx),
  444. 'parent'
  445. )
  446. self.assertEqual(
  447. template.Template('{% include "child" only %}').render(ctx),
  448. 'none'
  449. )
  450. def test_stack_size(self):
  451. """
  452. Regression test for #7116, Optimize RequetsContext construction
  453. """
  454. ctx = RequestContext(self.fake_request, {})
  455. # The stack should now contain 3 items:
  456. # [builtins, supplied context, context processor]
  457. self.assertEqual(len(ctx.dicts), 3)
  458. def test_context_comparable(self):
  459. # Create an engine without any context processors.
  460. engine = Engine()
  461. test_data = {'x': 'y', 'v': 'z', 'd': {'o': object, 'a': 'b'}}
  462. # test comparing RequestContext to prevent problems if somebody
  463. # adds __eq__ in the future
  464. request = RequestFactory().get('/')
  465. self.assertEqual(
  466. RequestContext(request, dict_=test_data, engine=engine),
  467. RequestContext(request, dict_=test_data, engine=engine))
  468. @ignore_warnings(category=RemovedInDjango20Warning)
  469. class SSITests(SimpleTestCase):
  470. def setUp(self):
  471. self.this_dir = os.path.dirname(os.path.abspath(upath(__file__)))
  472. self.ssi_dir = os.path.join(self.this_dir, "templates", "first")
  473. self.engine = Engine(allowed_include_roots=(self.ssi_dir,))
  474. def render_ssi(self, path):
  475. # the path must exist for the test to be reliable
  476. self.assertTrue(os.path.exists(path))
  477. return self.engine.from_string('{%% ssi "%s" %%}' % path).render(Context({}))
  478. def test_allowed_paths(self):
  479. acceptable_path = os.path.join(self.ssi_dir, "..", "first", "test.html")
  480. self.assertEqual(self.render_ssi(acceptable_path), 'First template\n')
  481. def test_relative_include_exploit(self):
  482. """
  483. May not bypass allowed_include_roots with relative paths
  484. e.g. if allowed_include_roots = ("/var/www",), it should not be
  485. possible to do {% ssi "/var/www/../../etc/passwd" %}
  486. """
  487. disallowed_paths = [
  488. os.path.join(self.ssi_dir, "..", "ssi_include.html"),
  489. os.path.join(self.ssi_dir, "..", "second", "test.html"),
  490. ]
  491. for disallowed_path in disallowed_paths:
  492. self.assertEqual(self.render_ssi(disallowed_path), '')