tests.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. from __future__ import unicode_literals
  2. import copy
  3. import logging
  4. import sys
  5. from unittest import skipUnless
  6. import warnings
  7. from django.conf import LazySettings
  8. from django.core import mail
  9. from django.test import TestCase, RequestFactory
  10. from django.test.utils import override_settings, patch_logger
  11. from django.utils.encoding import force_text
  12. from django.utils.log import (CallbackFilter, RequireDebugFalse,
  13. RequireDebugTrue)
  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 DeprecationWarnings is enabled
  74. 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 django/conf/__init__.py). However,
  79. # 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', DeprecationWarning)
  102. output = force_text(self.outputs[0].getvalue())
  103. self.assertTrue('Foo Deprecated' in output)
  104. def test_warnings_capture_debug_false(self):
  105. warnings.warn('Foo Deprecated', DeprecationWarning)
  106. output = force_text(self.outputs[0].getvalue())
  107. self.assertFalse('Foo Deprecated' in output)
  108. class CallbackFilterTest(TestCase):
  109. def test_sense(self):
  110. f_false = CallbackFilter(lambda r: False)
  111. f_true = CallbackFilter(lambda r: True)
  112. self.assertEqual(f_false.filter("record"), False)
  113. self.assertEqual(f_true.filter("record"), True)
  114. def test_passes_on_record(self):
  115. collector = []
  116. def _callback(record):
  117. collector.append(record)
  118. return True
  119. f = CallbackFilter(_callback)
  120. f.filter("a record")
  121. self.assertEqual(collector, ["a record"])
  122. class AdminEmailHandlerTest(TestCase):
  123. logger = logging.getLogger('django.request')
  124. def get_admin_email_handler(self, logger):
  125. # Inspired from views/views.py: send_log()
  126. # ensuring the AdminEmailHandler does not get filtered out
  127. # even with DEBUG=True.
  128. admin_email_handler = [
  129. h for h in logger.handlers
  130. if h.__class__.__name__ == "AdminEmailHandler"
  131. ][0]
  132. return admin_email_handler
  133. def test_fail_silently(self):
  134. admin_email_handler = self.get_admin_email_handler(self.logger)
  135. self.assertTrue(admin_email_handler.connection().fail_silently)
  136. @override_settings(
  137. ADMINS=(('whatever admin', 'admin@example.com'),),
  138. EMAIL_SUBJECT_PREFIX='-SuperAwesomeSubject-'
  139. )
  140. def test_accepts_args(self):
  141. """
  142. Ensure that user-supplied arguments and the EMAIL_SUBJECT_PREFIX
  143. setting are used to compose the email subject.
  144. Refs #16736.
  145. """
  146. message = "Custom message that says '%s' and '%s'"
  147. token1 = 'ping'
  148. token2 = 'pong'
  149. admin_email_handler = self.get_admin_email_handler(self.logger)
  150. # Backup then override original filters
  151. orig_filters = admin_email_handler.filters
  152. try:
  153. admin_email_handler.filters = []
  154. self.logger.error(message, token1, token2)
  155. self.assertEqual(len(mail.outbox), 1)
  156. self.assertEqual(mail.outbox[0].to, ['admin@example.com'])
  157. self.assertEqual(mail.outbox[0].subject,
  158. "-SuperAwesomeSubject-ERROR: Custom message that says 'ping' and 'pong'")
  159. finally:
  160. # Restore original filters
  161. admin_email_handler.filters = orig_filters
  162. @override_settings(
  163. ADMINS=(('whatever admin', 'admin@example.com'),),
  164. EMAIL_SUBJECT_PREFIX='-SuperAwesomeSubject-',
  165. INTERNAL_IPS=('127.0.0.1',),
  166. )
  167. def test_accepts_args_and_request(self):
  168. """
  169. Ensure that the subject is also handled if being
  170. passed a request object.
  171. """
  172. message = "Custom message that says '%s' and '%s'"
  173. token1 = 'ping'
  174. token2 = 'pong'
  175. admin_email_handler = self.get_admin_email_handler(self.logger)
  176. # Backup then override original filters
  177. orig_filters = admin_email_handler.filters
  178. try:
  179. admin_email_handler.filters = []
  180. rf = RequestFactory()
  181. request = rf.get('/')
  182. self.logger.error(message, token1, token2,
  183. extra={
  184. 'status_code': 403,
  185. 'request': request,
  186. }
  187. )
  188. self.assertEqual(len(mail.outbox), 1)
  189. self.assertEqual(mail.outbox[0].to, ['admin@example.com'])
  190. self.assertEqual(mail.outbox[0].subject,
  191. "-SuperAwesomeSubject-ERROR (internal IP): Custom message that says 'ping' and 'pong'")
  192. finally:
  193. # Restore original filters
  194. admin_email_handler.filters = orig_filters
  195. @override_settings(
  196. ADMINS=(('admin', 'admin@example.com'),),
  197. EMAIL_SUBJECT_PREFIX='',
  198. DEBUG=False,
  199. )
  200. def test_subject_accepts_newlines(self):
  201. """
  202. Ensure that newlines in email reports' subjects are escaped to avoid
  203. AdminErrorHandler to fail.
  204. Refs #17281.
  205. """
  206. message = 'Message \r\n with newlines'
  207. expected_subject = 'ERROR: Message \\r\\n with newlines'
  208. self.assertEqual(len(mail.outbox), 0)
  209. self.logger.error(message)
  210. self.assertEqual(len(mail.outbox), 1)
  211. self.assertFalse('\n' in mail.outbox[0].subject)
  212. self.assertFalse('\r' in mail.outbox[0].subject)
  213. self.assertEqual(mail.outbox[0].subject, expected_subject)
  214. @override_settings(
  215. ADMINS=(('admin', 'admin@example.com'),),
  216. EMAIL_SUBJECT_PREFIX='',
  217. DEBUG=False,
  218. )
  219. def test_truncate_subject(self):
  220. """
  221. RFC 2822's hard limit is 998 characters per line.
  222. So, minus "Subject: ", the actual subject must be no longer than 989
  223. characters.
  224. Refs #17281.
  225. """
  226. message = 'a' * 1000
  227. expected_subject = 'ERROR: aa' + 'a' * 980
  228. self.assertEqual(len(mail.outbox), 0)
  229. self.logger.error(message)
  230. self.assertEqual(len(mail.outbox), 1)
  231. self.assertEqual(mail.outbox[0].subject, expected_subject)
  232. @override_settings(
  233. ADMINS=(('admin', 'admin@example.com'),),
  234. DEBUG=False,
  235. )
  236. def test_uses_custom_email_backend(self):
  237. """
  238. Refs #19325
  239. """
  240. message = 'All work and no play makes Jack a dull boy'
  241. admin_email_handler = self.get_admin_email_handler(self.logger)
  242. mail_admins_called = {'called': False}
  243. def my_mail_admins(*args, **kwargs):
  244. connection = kwargs['connection']
  245. self.assertIsInstance(connection, MyEmailBackend)
  246. mail_admins_called['called'] = True
  247. # Monkeypatches
  248. orig_mail_admins = mail.mail_admins
  249. orig_email_backend = admin_email_handler.email_backend
  250. mail.mail_admins = my_mail_admins
  251. admin_email_handler.email_backend = (
  252. 'logging_tests.logconfig.MyEmailBackend')
  253. try:
  254. self.logger.error(message)
  255. self.assertTrue(mail_admins_called['called'])
  256. finally:
  257. # Revert Monkeypatches
  258. mail.mail_admins = orig_mail_admins
  259. admin_email_handler.email_backend = orig_email_backend
  260. class SettingsConfigTest(AdminScriptTestCase):
  261. """
  262. Test that accessing settings in a custom logging handler does not trigger
  263. a circular import error.
  264. """
  265. def setUp(self):
  266. log_config = """{
  267. 'version': 1,
  268. 'handlers': {
  269. 'custom_handler': {
  270. 'level': 'INFO',
  271. 'class': 'logging_tests.logconfig.MyHandler',
  272. }
  273. }
  274. }"""
  275. self.write_settings('settings.py', sdict={'LOGGING': log_config})
  276. def tearDown(self):
  277. self.remove_settings('settings.py')
  278. def test_circular_dependency(self):
  279. # validate is just an example command to trigger settings configuration
  280. out, err = self.run_manage(['validate'])
  281. self.assertNoOutput(err)
  282. self.assertOutput(out, "0 errors found")
  283. def dictConfig(config):
  284. dictConfig.called = True
  285. dictConfig.called = False
  286. class SettingsConfigureLogging(TestCase):
  287. """
  288. Test that calling settings.configure() initializes the logging
  289. configuration.
  290. """
  291. def test_configure_initializes_logging(self):
  292. settings = LazySettings()
  293. settings.configure(
  294. LOGGING_CONFIG='logging_tests.tests.dictConfig')
  295. self.assertTrue(dictConfig.called)
  296. class SecurityLoggerTest(TestCase):
  297. urls = 'logging_tests.urls'
  298. def test_suspicious_operation_creates_log_message(self):
  299. with self.settings(DEBUG=True):
  300. with patch_logger('django.security.SuspiciousOperation', 'error') as calls:
  301. response = self.client.get('/suspicious/')
  302. self.assertEqual(len(calls), 1)
  303. self.assertEqual(calls[0], 'dubious')
  304. def test_suspicious_operation_uses_sublogger(self):
  305. with self.settings(DEBUG=True):
  306. with patch_logger('django.security.DisallowedHost', 'error') as calls:
  307. response = self.client.get('/suspicious_spec/')
  308. self.assertEqual(len(calls), 1)
  309. self.assertEqual(calls[0], 'dubious')