tests.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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. PYVERS = sys.version_info[:2]
  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(TestCase):
  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 DefaultLoggingTest(TestCase):
  56. def setUp(self):
  57. self.logger = logging.getLogger('django')
  58. self.old_stream = self.logger.handlers[0].stream
  59. def tearDown(self):
  60. self.logger.handlers[0].stream = self.old_stream
  61. def test_django_logger(self):
  62. """
  63. The 'django' base logger only output anything when DEBUG=True.
  64. """
  65. output = StringIO()
  66. self.logger.handlers[0].stream = output
  67. self.logger.error("Hey, this is an error.")
  68. self.assertEqual(output.getvalue(), '')
  69. with self.settings(DEBUG=True):
  70. self.logger.error("Hey, this is an error.")
  71. self.assertEqual(output.getvalue(), 'Hey, this is an error.\n')
  72. @skipUnless(PYVERS > (2,6), "warnings captured only in Python >= 2.7")
  73. class WarningLoggerTests(TestCase):
  74. """
  75. Tests that warnings output for DeprecationWarnings is enabled
  76. and captured to the logging system
  77. """
  78. def setUp(self):
  79. # If tests are invoke with "-Wall" (or any -W flag actually) then
  80. # warning logging gets disabled (see django/conf/__init__.py). However,
  81. # these tests expect warnings to be logged, so manually force warnings
  82. # to the logs. Use getattr() here because the logging capture state is
  83. # undocumented and (I assume) brittle.
  84. self._old_capture_state = bool(getattr(logging, '_warnings_showwarning', False))
  85. logging.captureWarnings(True)
  86. # this convoluted setup is to avoid printing this deprecation to
  87. # stderr during test running - as the test runner forces deprecations
  88. # to be displayed at the global py.warnings level
  89. self.logger = logging.getLogger('py.warnings')
  90. self.outputs = []
  91. self.old_streams = []
  92. for handler in self.logger.handlers:
  93. self.old_streams.append(handler.stream)
  94. self.outputs.append(StringIO())
  95. handler.stream = self.outputs[-1]
  96. def tearDown(self):
  97. for i, handler in enumerate(self.logger.handlers):
  98. self.logger.handlers[i].stream = self.old_streams[i]
  99. # Reset warnings state.
  100. logging.captureWarnings(self._old_capture_state)
  101. @override_settings(DEBUG=True)
  102. def test_warnings_capture(self):
  103. warnings.warn('Foo Deprecated', DeprecationWarning)
  104. output = force_text(self.outputs[0].getvalue())
  105. self.assertTrue('Foo Deprecated' in output)
  106. def test_warnings_capture_debug_false(self):
  107. warnings.warn('Foo Deprecated', DeprecationWarning)
  108. output = force_text(self.outputs[0].getvalue())
  109. self.assertFalse('Foo Deprecated' in output)
  110. class CallbackFilterTest(TestCase):
  111. def test_sense(self):
  112. f_false = CallbackFilter(lambda r: False)
  113. f_true = CallbackFilter(lambda r: True)
  114. self.assertEqual(f_false.filter("record"), False)
  115. self.assertEqual(f_true.filter("record"), True)
  116. def test_passes_on_record(self):
  117. collector = []
  118. def _callback(record):
  119. collector.append(record)
  120. return True
  121. f = CallbackFilter(_callback)
  122. f.filter("a record")
  123. self.assertEqual(collector, ["a record"])
  124. class AdminEmailHandlerTest(TestCase):
  125. logger = logging.getLogger('django.request')
  126. def get_admin_email_handler(self, logger):
  127. # Inspired from views/views.py: send_log()
  128. # ensuring the AdminEmailHandler does not get filtered out
  129. # even with DEBUG=True.
  130. admin_email_handler = [
  131. h for h in logger.handlers
  132. if h.__class__.__name__ == "AdminEmailHandler"
  133. ][0]
  134. return admin_email_handler
  135. def test_fail_silently(self):
  136. admin_email_handler = self.get_admin_email_handler(self.logger)
  137. self.assertTrue(admin_email_handler.connection().fail_silently)
  138. @override_settings(
  139. ADMINS=(('whatever admin', 'admin@example.com'),),
  140. EMAIL_SUBJECT_PREFIX='-SuperAwesomeSubject-'
  141. )
  142. def test_accepts_args(self):
  143. """
  144. Ensure that user-supplied arguments and the EMAIL_SUBJECT_PREFIX
  145. setting are used to compose the email subject.
  146. Refs #16736.
  147. """
  148. message = "Custom message that says '%s' and '%s'"
  149. token1 = 'ping'
  150. token2 = 'pong'
  151. admin_email_handler = self.get_admin_email_handler(self.logger)
  152. # Backup then override original filters
  153. orig_filters = admin_email_handler.filters
  154. try:
  155. admin_email_handler.filters = []
  156. self.logger.error(message, token1, token2)
  157. self.assertEqual(len(mail.outbox), 1)
  158. self.assertEqual(mail.outbox[0].to, ['admin@example.com'])
  159. self.assertEqual(mail.outbox[0].subject,
  160. "-SuperAwesomeSubject-ERROR: Custom message that says 'ping' and 'pong'")
  161. finally:
  162. # Restore original filters
  163. admin_email_handler.filters = orig_filters
  164. @override_settings(
  165. ADMINS=(('whatever admin', 'admin@example.com'),),
  166. EMAIL_SUBJECT_PREFIX='-SuperAwesomeSubject-',
  167. INTERNAL_IPS=('127.0.0.1',),
  168. )
  169. def test_accepts_args_and_request(self):
  170. """
  171. Ensure that the subject is also handled if being
  172. passed a request object.
  173. """
  174. message = "Custom message that says '%s' and '%s'"
  175. token1 = 'ping'
  176. token2 = 'pong'
  177. admin_email_handler = self.get_admin_email_handler(self.logger)
  178. # Backup then override original filters
  179. orig_filters = admin_email_handler.filters
  180. try:
  181. admin_email_handler.filters = []
  182. rf = RequestFactory()
  183. request = rf.get('/')
  184. self.logger.error(message, token1, token2,
  185. extra={
  186. 'status_code': 403,
  187. 'request': request,
  188. }
  189. )
  190. self.assertEqual(len(mail.outbox), 1)
  191. self.assertEqual(mail.outbox[0].to, ['admin@example.com'])
  192. self.assertEqual(mail.outbox[0].subject,
  193. "-SuperAwesomeSubject-ERROR (internal IP): Custom message that says 'ping' and 'pong'")
  194. finally:
  195. # Restore original filters
  196. admin_email_handler.filters = orig_filters
  197. @override_settings(
  198. ADMINS=(('admin', 'admin@example.com'),),
  199. EMAIL_SUBJECT_PREFIX='',
  200. DEBUG=False,
  201. )
  202. def test_subject_accepts_newlines(self):
  203. """
  204. Ensure that newlines in email reports' subjects are escaped to avoid
  205. AdminErrorHandler to fail.
  206. Refs #17281.
  207. """
  208. message = 'Message \r\n with newlines'
  209. expected_subject = 'ERROR: Message \\r\\n with newlines'
  210. self.assertEqual(len(mail.outbox), 0)
  211. self.logger.error(message)
  212. self.assertEqual(len(mail.outbox), 1)
  213. self.assertFalse('\n' in mail.outbox[0].subject)
  214. self.assertFalse('\r' in mail.outbox[0].subject)
  215. self.assertEqual(mail.outbox[0].subject, expected_subject)
  216. @override_settings(
  217. ADMINS=(('admin', 'admin@example.com'),),
  218. EMAIL_SUBJECT_PREFIX='',
  219. DEBUG=False,
  220. )
  221. def test_truncate_subject(self):
  222. """
  223. RFC 2822's hard limit is 998 characters per line.
  224. So, minus "Subject: ", the actual subject must be no longer than 989
  225. characters.
  226. Refs #17281.
  227. """
  228. message = 'a' * 1000
  229. expected_subject = 'ERROR: aa' + 'a' * 980
  230. self.assertEqual(len(mail.outbox), 0)
  231. self.logger.error(message)
  232. self.assertEqual(len(mail.outbox), 1)
  233. self.assertEqual(mail.outbox[0].subject, expected_subject)
  234. @override_settings(
  235. ADMINS=(('admin', 'admin@example.com'),),
  236. DEBUG=False,
  237. )
  238. def test_uses_custom_email_backend(self):
  239. """
  240. Refs #19325
  241. """
  242. message = 'All work and no play makes Jack a dull boy'
  243. admin_email_handler = self.get_admin_email_handler(self.logger)
  244. mail_admins_called = {'called': False}
  245. def my_mail_admins(*args, **kwargs):
  246. connection = kwargs['connection']
  247. self.assertIsInstance(connection, MyEmailBackend)
  248. mail_admins_called['called'] = True
  249. # Monkeypatches
  250. orig_mail_admins = mail.mail_admins
  251. orig_email_backend = admin_email_handler.email_backend
  252. mail.mail_admins = my_mail_admins
  253. admin_email_handler.email_backend = (
  254. 'logging_tests.logconfig.MyEmailBackend')
  255. try:
  256. self.logger.error(message)
  257. self.assertTrue(mail_admins_called['called'])
  258. finally:
  259. # Revert Monkeypatches
  260. mail.mail_admins = orig_mail_admins
  261. admin_email_handler.email_backend = orig_email_backend
  262. class SettingsConfigTest(AdminScriptTestCase):
  263. """
  264. Test that accessing settings in a custom logging handler does not trigger
  265. a circular import error.
  266. """
  267. def setUp(self):
  268. log_config = """{
  269. 'version': 1,
  270. 'handlers': {
  271. 'custom_handler': {
  272. 'level': 'INFO',
  273. 'class': 'logging_tests.logconfig.MyHandler',
  274. }
  275. }
  276. }"""
  277. self.write_settings('settings.py', sdict={'LOGGING': log_config})
  278. def tearDown(self):
  279. self.remove_settings('settings.py')
  280. def test_circular_dependency(self):
  281. # validate is just an example command to trigger settings configuration
  282. out, err = self.run_manage(['validate'])
  283. self.assertNoOutput(err)
  284. self.assertOutput(out, "0 errors found")
  285. def dictConfig(config):
  286. dictConfig.called = True
  287. dictConfig.called = False
  288. class SettingsConfigureLogging(TestCase):
  289. """
  290. Test that calling settings.configure() initializes the logging
  291. configuration.
  292. """
  293. def test_configure_initializes_logging(self):
  294. settings = LazySettings()
  295. settings.configure(
  296. LOGGING_CONFIG='logging_tests.tests.dictConfig')
  297. self.assertTrue(dictConfig.called)
  298. class SecurityLoggerTest(TestCase):
  299. urls = 'logging_tests.urls'
  300. def test_suspicious_operation_creates_log_message(self):
  301. with self.settings(DEBUG=True):
  302. with patch_logger('django.security.SuspiciousOperation', 'error') as calls:
  303. response = self.client.get('/suspicious/')
  304. self.assertEqual(len(calls), 1)
  305. self.assertEqual(calls[0], 'dubious')
  306. def test_suspicious_operation_uses_sublogger(self):
  307. with self.settings(DEBUG=True):
  308. with patch_logger('django.security.DisallowedHost', 'error') as calls:
  309. response = self.client.get('/suspicious_spec/')
  310. self.assertEqual(len(calls), 1)
  311. self.assertEqual(calls[0], 'dubious')