tests.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. # -*- coding:utf-8 -*-
  2. from __future__ import unicode_literals
  3. import logging
  4. import warnings
  5. from django.core import mail
  6. from django.core.files.temp import NamedTemporaryFile
  7. from django.test import TestCase, RequestFactory, override_settings
  8. from django.test.utils import patch_logger
  9. from django.utils.encoding import force_text
  10. from django.utils.deprecation import RemovedInNextVersionWarning
  11. from django.utils.log import (
  12. AdminEmailHandler, CallbackFilter, RequireDebugFalse, RequireDebugTrue,
  13. )
  14. from django.utils.six import StringIO
  15. from admin_scripts.tests import AdminScriptTestCase
  16. from .logconfig import MyEmailBackend
  17. # logging config prior to using filter with mail_admins
  18. OLD_LOGGING = {
  19. 'version': 1,
  20. 'disable_existing_loggers': False,
  21. 'handlers': {
  22. 'mail_admins': {
  23. 'level': 'ERROR',
  24. 'class': 'django.utils.log.AdminEmailHandler'
  25. }
  26. },
  27. 'loggers': {
  28. 'django.request': {
  29. 'handlers': ['mail_admins'],
  30. 'level': 'ERROR',
  31. 'propagate': True,
  32. },
  33. }
  34. }
  35. class LoggingFiltersTest(TestCase):
  36. def test_require_debug_false_filter(self):
  37. """
  38. Test the RequireDebugFalse filter class.
  39. """
  40. filter_ = RequireDebugFalse()
  41. with self.settings(DEBUG=True):
  42. self.assertEqual(filter_.filter("record is not used"), False)
  43. with self.settings(DEBUG=False):
  44. self.assertEqual(filter_.filter("record is not used"), True)
  45. def test_require_debug_true_filter(self):
  46. """
  47. Test the RequireDebugTrue filter class.
  48. """
  49. filter_ = RequireDebugTrue()
  50. with self.settings(DEBUG=True):
  51. self.assertEqual(filter_.filter("record is not used"), True)
  52. with self.settings(DEBUG=False):
  53. self.assertEqual(filter_.filter("record is not used"), False)
  54. class DefaultLoggingTest(TestCase):
  55. def setUp(self):
  56. self.logger = logging.getLogger('django')
  57. self.old_stream = self.logger.handlers[0].stream
  58. def tearDown(self):
  59. self.logger.handlers[0].stream = self.old_stream
  60. def test_django_logger(self):
  61. """
  62. The 'django' base logger only output anything when DEBUG=True.
  63. """
  64. output = StringIO()
  65. self.logger.handlers[0].stream = output
  66. self.logger.error("Hey, this is an error.")
  67. self.assertEqual(output.getvalue(), '')
  68. with self.settings(DEBUG=True):
  69. self.logger.error("Hey, this is an error.")
  70. self.assertEqual(output.getvalue(), 'Hey, this is an error.\n')
  71. class WarningLoggerTests(TestCase):
  72. """
  73. Tests that warnings output for RemovedInDjangoXXWarning (XX being the next
  74. Django version) is enabled and captured to the logging system
  75. """
  76. def setUp(self):
  77. # If tests are invoke with "-Wall" (or any -W flag actually) then
  78. # warning logging gets disabled (see configure_logging in django/utils/log.py).
  79. # However, these tests expect warnings to be logged, so manually force warnings
  80. # to the logs. Use getattr() here because the logging capture state is
  81. # undocumented and (I assume) brittle.
  82. self._old_capture_state = bool(getattr(logging, '_warnings_showwarning', False))
  83. logging.captureWarnings(True)
  84. # this convoluted setup is to avoid printing this deprecation to
  85. # stderr during test running - as the test runner forces deprecations
  86. # to be displayed at the global py.warnings level
  87. self.logger = logging.getLogger('py.warnings')
  88. self.outputs = []
  89. self.old_streams = []
  90. for handler in self.logger.handlers:
  91. self.old_streams.append(handler.stream)
  92. self.outputs.append(StringIO())
  93. handler.stream = self.outputs[-1]
  94. def tearDown(self):
  95. for i, handler in enumerate(self.logger.handlers):
  96. self.logger.handlers[i].stream = self.old_streams[i]
  97. # Reset warnings state.
  98. logging.captureWarnings(self._old_capture_state)
  99. @override_settings(DEBUG=True)
  100. def test_warnings_capture(self):
  101. warnings.warn('Foo Deprecated', RemovedInNextVersionWarning)
  102. output = force_text(self.outputs[0].getvalue())
  103. self.assertIn('Foo Deprecated', output)
  104. def test_warnings_capture_debug_false(self):
  105. warnings.warn('Foo Deprecated', RemovedInNextVersionWarning)
  106. output = force_text(self.outputs[0].getvalue())
  107. self.assertNotIn('Foo Deprecated', output)
  108. @override_settings(DEBUG=True)
  109. def test_error_filter_still_raises(self):
  110. with warnings.catch_warnings():
  111. warnings.filterwarnings(
  112. 'error',
  113. category=RemovedInNextVersionWarning
  114. )
  115. with self.assertRaises(RemovedInNextVersionWarning):
  116. warnings.warn('Foo Deprecated', RemovedInNextVersionWarning)
  117. class CallbackFilterTest(TestCase):
  118. def test_sense(self):
  119. f_false = CallbackFilter(lambda r: False)
  120. f_true = CallbackFilter(lambda r: True)
  121. self.assertEqual(f_false.filter("record"), False)
  122. self.assertEqual(f_true.filter("record"), True)
  123. def test_passes_on_record(self):
  124. collector = []
  125. def _callback(record):
  126. collector.append(record)
  127. return True
  128. f = CallbackFilter(_callback)
  129. f.filter("a record")
  130. self.assertEqual(collector, ["a record"])
  131. class AdminEmailHandlerTest(TestCase):
  132. logger = logging.getLogger('django.request')
  133. def get_admin_email_handler(self, logger):
  134. # Inspired from views/views.py: send_log()
  135. # ensuring the AdminEmailHandler does not get filtered out
  136. # even with DEBUG=True.
  137. admin_email_handler = [
  138. h for h in logger.handlers
  139. if h.__class__.__name__ == "AdminEmailHandler"
  140. ][0]
  141. return admin_email_handler
  142. def test_fail_silently(self):
  143. admin_email_handler = self.get_admin_email_handler(self.logger)
  144. self.assertTrue(admin_email_handler.connection().fail_silently)
  145. @override_settings(
  146. ADMINS=(('whatever admin', 'admin@example.com'),),
  147. EMAIL_SUBJECT_PREFIX='-SuperAwesomeSubject-'
  148. )
  149. def test_accepts_args(self):
  150. """
  151. Ensure that user-supplied arguments and the EMAIL_SUBJECT_PREFIX
  152. setting are used to compose the email subject.
  153. Refs #16736.
  154. """
  155. message = "Custom message that says '%s' and '%s'"
  156. token1 = 'ping'
  157. token2 = 'pong'
  158. admin_email_handler = self.get_admin_email_handler(self.logger)
  159. # Backup then override original filters
  160. orig_filters = admin_email_handler.filters
  161. try:
  162. admin_email_handler.filters = []
  163. self.logger.error(message, token1, token2)
  164. self.assertEqual(len(mail.outbox), 1)
  165. self.assertEqual(mail.outbox[0].to, ['admin@example.com'])
  166. self.assertEqual(mail.outbox[0].subject,
  167. "-SuperAwesomeSubject-ERROR: Custom message that says 'ping' and 'pong'")
  168. finally:
  169. # Restore original filters
  170. admin_email_handler.filters = orig_filters
  171. @override_settings(
  172. ADMINS=(('whatever admin', 'admin@example.com'),),
  173. EMAIL_SUBJECT_PREFIX='-SuperAwesomeSubject-',
  174. INTERNAL_IPS=('127.0.0.1',),
  175. )
  176. def test_accepts_args_and_request(self):
  177. """
  178. Ensure that the subject is also handled if being
  179. passed a request object.
  180. """
  181. message = "Custom message that says '%s' and '%s'"
  182. token1 = 'ping'
  183. token2 = 'pong'
  184. admin_email_handler = self.get_admin_email_handler(self.logger)
  185. # Backup then override original filters
  186. orig_filters = admin_email_handler.filters
  187. try:
  188. admin_email_handler.filters = []
  189. rf = RequestFactory()
  190. request = rf.get('/')
  191. self.logger.error(message, token1, token2,
  192. extra={
  193. 'status_code': 403,
  194. 'request': request,
  195. }
  196. )
  197. self.assertEqual(len(mail.outbox), 1)
  198. self.assertEqual(mail.outbox[0].to, ['admin@example.com'])
  199. self.assertEqual(mail.outbox[0].subject,
  200. "-SuperAwesomeSubject-ERROR (internal IP): Custom message that says 'ping' and 'pong'")
  201. finally:
  202. # Restore original filters
  203. admin_email_handler.filters = orig_filters
  204. @override_settings(
  205. ADMINS=(('admin', 'admin@example.com'),),
  206. EMAIL_SUBJECT_PREFIX='',
  207. DEBUG=False,
  208. )
  209. def test_subject_accepts_newlines(self):
  210. """
  211. Ensure that newlines in email reports' subjects are escaped to avoid
  212. AdminErrorHandler to fail.
  213. Refs #17281.
  214. """
  215. message = 'Message \r\n with newlines'
  216. expected_subject = 'ERROR: Message \\r\\n with newlines'
  217. self.assertEqual(len(mail.outbox), 0)
  218. self.logger.error(message)
  219. self.assertEqual(len(mail.outbox), 1)
  220. self.assertNotIn('\n', mail.outbox[0].subject)
  221. self.assertNotIn('\r', mail.outbox[0].subject)
  222. self.assertEqual(mail.outbox[0].subject, expected_subject)
  223. @override_settings(
  224. ADMINS=(('admin', 'admin@example.com'),),
  225. EMAIL_SUBJECT_PREFIX='',
  226. DEBUG=False,
  227. )
  228. def test_truncate_subject(self):
  229. """
  230. RFC 2822's hard limit is 998 characters per line.
  231. So, minus "Subject: ", the actual subject must be no longer than 989
  232. characters.
  233. Refs #17281.
  234. """
  235. message = 'a' * 1000
  236. expected_subject = 'ERROR: aa' + 'a' * 980
  237. self.assertEqual(len(mail.outbox), 0)
  238. self.logger.error(message)
  239. self.assertEqual(len(mail.outbox), 1)
  240. self.assertEqual(mail.outbox[0].subject, expected_subject)
  241. @override_settings(
  242. ADMINS=(('admin', 'admin@example.com'),),
  243. DEBUG=False,
  244. )
  245. def test_uses_custom_email_backend(self):
  246. """
  247. Refs #19325
  248. """
  249. message = 'All work and no play makes Jack a dull boy'
  250. admin_email_handler = self.get_admin_email_handler(self.logger)
  251. mail_admins_called = {'called': False}
  252. def my_mail_admins(*args, **kwargs):
  253. connection = kwargs['connection']
  254. self.assertIsInstance(connection, MyEmailBackend)
  255. mail_admins_called['called'] = True
  256. # Monkeypatches
  257. orig_mail_admins = mail.mail_admins
  258. orig_email_backend = admin_email_handler.email_backend
  259. mail.mail_admins = my_mail_admins
  260. admin_email_handler.email_backend = (
  261. 'logging_tests.logconfig.MyEmailBackend')
  262. try:
  263. self.logger.error(message)
  264. self.assertTrue(mail_admins_called['called'])
  265. finally:
  266. # Revert Monkeypatches
  267. mail.mail_admins = orig_mail_admins
  268. admin_email_handler.email_backend = orig_email_backend
  269. @override_settings(
  270. ADMINS=(('whatever admin', 'admin@example.com'),),
  271. )
  272. def test_emit_non_ascii(self):
  273. """
  274. #23593 - AdminEmailHandler should allow Unicode characters in the
  275. request.
  276. """
  277. handler = self.get_admin_email_handler(self.logger)
  278. record = self.logger.makeRecord('name', logging.ERROR, 'function', 'lno', 'message', None, None)
  279. rf = RequestFactory()
  280. url_path = '/º'
  281. record.request = rf.get(url_path)
  282. handler.emit(record)
  283. self.assertEqual(len(mail.outbox), 1)
  284. msg = mail.outbox[0]
  285. self.assertEqual(msg.to, ['admin@example.com'])
  286. self.assertEqual(msg.subject, "[Django] ERROR (EXTERNAL IP): message")
  287. self.assertIn("path:%s" % url_path, msg.body)
  288. @override_settings(
  289. MANAGERS=(('manager', 'manager@example.com'),),
  290. DEBUG=False,
  291. )
  292. def test_customize_send_mail_method(self):
  293. class ManagerEmailHandler(AdminEmailHandler):
  294. def send_mail(self, subject, message, *args, **kwargs):
  295. mail.mail_managers(subject, message, *args, connection=self.connection(), **kwargs)
  296. handler = ManagerEmailHandler()
  297. record = self.logger.makeRecord('name', logging.ERROR, 'function', 'lno', 'message', None, None)
  298. self.assertEqual(len(mail.outbox), 0)
  299. handler.emit(record)
  300. self.assertEqual(len(mail.outbox), 1)
  301. self.assertEqual(mail.outbox[0].to, ['manager@example.com'])
  302. class SettingsConfigTest(AdminScriptTestCase):
  303. """
  304. Test that accessing settings in a custom logging handler does not trigger
  305. a circular import error.
  306. """
  307. def setUp(self):
  308. log_config = """{
  309. 'version': 1,
  310. 'handlers': {
  311. 'custom_handler': {
  312. 'level': 'INFO',
  313. 'class': 'logging_tests.logconfig.MyHandler',
  314. }
  315. }
  316. }"""
  317. self.write_settings('settings.py', sdict={'LOGGING': log_config})
  318. def tearDown(self):
  319. self.remove_settings('settings.py')
  320. def test_circular_dependency(self):
  321. # validate is just an example command to trigger settings configuration
  322. out, err = self.run_manage(['validate'])
  323. self.assertNoOutput(err)
  324. self.assertOutput(out, "System check identified no issues (0 silenced).")
  325. def dictConfig(config):
  326. dictConfig.called = True
  327. dictConfig.called = False
  328. class SetupConfigureLogging(TestCase):
  329. """
  330. Test that calling django.setup() initializes the logging configuration.
  331. """
  332. @override_settings(LOGGING_CONFIG='logging_tests.tests.dictConfig',
  333. LOGGING=OLD_LOGGING)
  334. def test_configure_initializes_logging(self):
  335. from django import setup
  336. setup()
  337. self.assertTrue(dictConfig.called)
  338. @override_settings(DEBUG=True, ROOT_URLCONF='logging_tests.urls')
  339. class SecurityLoggerTest(TestCase):
  340. def test_suspicious_operation_creates_log_message(self):
  341. with patch_logger('django.security.SuspiciousOperation', 'error') as calls:
  342. self.client.get('/suspicious/')
  343. self.assertEqual(len(calls), 1)
  344. self.assertEqual(calls[0], 'dubious')
  345. def test_suspicious_operation_uses_sublogger(self):
  346. with patch_logger('django.security.DisallowedHost', 'error') as calls:
  347. self.client.get('/suspicious_spec/')
  348. self.assertEqual(len(calls), 1)
  349. self.assertEqual(calls[0], 'dubious')
  350. @override_settings(
  351. ADMINS=(('admin', 'admin@example.com'),),
  352. DEBUG=False,
  353. )
  354. def test_suspicious_email_admins(self):
  355. self.client.get('/suspicious/')
  356. self.assertEqual(len(mail.outbox), 1)
  357. self.assertIn('path:/suspicious/,', mail.outbox[0].body)
  358. class SettingsCustomLoggingTest(AdminScriptTestCase):
  359. """
  360. Test that using a logging defaults are still applied when using a custom
  361. callable in LOGGING_CONFIG (i.e., logging.config.fileConfig).
  362. """
  363. def setUp(self):
  364. logging_conf = """
  365. [loggers]
  366. keys=root
  367. [handlers]
  368. keys=stream
  369. [formatters]
  370. keys=simple
  371. [logger_root]
  372. handlers=stream
  373. [handler_stream]
  374. class=StreamHandler
  375. formatter=simple
  376. args=(sys.stdout,)
  377. [formatter_simple]
  378. format=%(message)s
  379. """
  380. self.temp_file = NamedTemporaryFile()
  381. self.temp_file.write(logging_conf.encode('utf-8'))
  382. self.temp_file.flush()
  383. sdict = {'LOGGING_CONFIG': '"logging.config.fileConfig"',
  384. 'LOGGING': 'r"%s"' % self.temp_file.name}
  385. self.write_settings('settings.py', sdict=sdict)
  386. def tearDown(self):
  387. self.temp_file.close()
  388. self.remove_settings('settings.py')
  389. def test_custom_logging(self):
  390. out, err = self.run_manage(['validate'])
  391. self.assertNoOutput(err)
  392. self.assertOutput(out, "System check identified no issues (0 silenced).")