tests.py 17 KB

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