tests.py 16 KB

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