test_cookie.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import json
  2. from django.contrib.messages import constants
  3. from django.contrib.messages.storage.base import Message
  4. from django.contrib.messages.storage.cookie import (
  5. CookieStorage, MessageDecoder, MessageEncoder,
  6. )
  7. from django.test import SimpleTestCase, override_settings
  8. from django.utils.safestring import SafeData, mark_safe
  9. from .base import BaseTests
  10. def set_cookie_data(storage, messages, invalid=False, encode_empty=False):
  11. """
  12. Set ``request.COOKIES`` with the encoded data and remove the storage
  13. backend's loaded data cache.
  14. """
  15. encoded_data = storage._encode(messages, encode_empty=encode_empty)
  16. if invalid:
  17. # Truncate the first character so that the hash is invalid.
  18. encoded_data = encoded_data[1:]
  19. storage.request.COOKIES = {CookieStorage.cookie_name: encoded_data}
  20. if hasattr(storage, '_loaded_data'):
  21. del storage._loaded_data
  22. def stored_cookie_messages_count(storage, response):
  23. """
  24. Return an integer containing the number of messages stored.
  25. """
  26. # Get a list of cookies, excluding ones with a max-age of 0 (because
  27. # they have been marked for deletion).
  28. cookie = response.cookies.get(storage.cookie_name)
  29. if not cookie or cookie['max-age'] == 0:
  30. return 0
  31. data = storage._decode(cookie.value)
  32. if not data:
  33. return 0
  34. if data[-1] == CookieStorage.not_finished:
  35. data.pop()
  36. return len(data)
  37. @override_settings(SESSION_COOKIE_DOMAIN='.example.com', SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True)
  38. class CookieTests(BaseTests, SimpleTestCase):
  39. storage_class = CookieStorage
  40. def stored_messages_count(self, storage, response):
  41. return stored_cookie_messages_count(storage, response)
  42. def test_get(self):
  43. storage = self.storage_class(self.get_request())
  44. # Set initial data.
  45. example_messages = ['test', 'me']
  46. set_cookie_data(storage, example_messages)
  47. # The message contains what's expected.
  48. self.assertEqual(list(storage), example_messages)
  49. @override_settings(SESSION_COOKIE_SAMESITE='Strict')
  50. def test_cookie_setings(self):
  51. """
  52. CookieStorage honors SESSION_COOKIE_DOMAIN, SESSION_COOKIE_SECURE, and
  53. SESSION_COOKIE_HTTPONLY (#15618, #20972).
  54. """
  55. # Test before the messages have been consumed
  56. storage = self.get_storage()
  57. response = self.get_response()
  58. storage.add(constants.INFO, 'test')
  59. storage.update(response)
  60. self.assertIn('test', response.cookies['messages'].value)
  61. self.assertEqual(response.cookies['messages']['domain'], '.example.com')
  62. self.assertEqual(response.cookies['messages']['expires'], '')
  63. self.assertIs(response.cookies['messages']['secure'], True)
  64. self.assertIs(response.cookies['messages']['httponly'], True)
  65. self.assertEqual(response.cookies['messages']['samesite'], 'Strict')
  66. # Test deletion of the cookie (storing with an empty value) after the messages have been consumed
  67. storage = self.get_storage()
  68. response = self.get_response()
  69. storage.add(constants.INFO, 'test')
  70. for m in storage:
  71. pass # Iterate through the storage to simulate consumption of messages.
  72. storage.update(response)
  73. self.assertEqual(response.cookies['messages'].value, '')
  74. self.assertEqual(response.cookies['messages']['domain'], '.example.com')
  75. self.assertEqual(response.cookies['messages']['expires'], 'Thu, 01 Jan 1970 00:00:00 GMT')
  76. def test_get_bad_cookie(self):
  77. request = self.get_request()
  78. storage = self.storage_class(request)
  79. # Set initial (invalid) data.
  80. example_messages = ['test', 'me']
  81. set_cookie_data(storage, example_messages, invalid=True)
  82. # The message actually contains what we expect.
  83. self.assertEqual(list(storage), [])
  84. def test_max_cookie_length(self):
  85. """
  86. If the data exceeds what is allowed in a cookie, older messages are
  87. removed before saving (and returned by the ``update`` method).
  88. """
  89. storage = self.get_storage()
  90. response = self.get_response()
  91. # When storing as a cookie, the cookie has constant overhead of approx
  92. # 54 chars, and each message has a constant overhead of about 37 chars
  93. # and a variable overhead of zero in the best case. We aim for a message
  94. # size which will fit 4 messages into the cookie, but not 5.
  95. # See also FallbackTest.test_session_fallback
  96. msg_size = int((CookieStorage.max_cookie_size - 54) / 4.5 - 37)
  97. for i in range(5):
  98. storage.add(constants.INFO, str(i) * msg_size)
  99. unstored_messages = storage.update(response)
  100. cookie_storing = self.stored_messages_count(storage, response)
  101. self.assertEqual(cookie_storing, 4)
  102. self.assertEqual(len(unstored_messages), 1)
  103. self.assertEqual(unstored_messages[0].message, '0' * msg_size)
  104. def test_json_encoder_decoder(self):
  105. """
  106. A complex nested data structure containing Message
  107. instances is properly encoded/decoded by the custom JSON
  108. encoder/decoder classes.
  109. """
  110. messages = [
  111. {
  112. 'message': Message(constants.INFO, 'Test message'),
  113. 'message_list': [
  114. Message(constants.INFO, 'message %s') for x in range(5)
  115. ] + [{'another-message': Message(constants.ERROR, 'error')}],
  116. },
  117. Message(constants.INFO, 'message %s'),
  118. ]
  119. encoder = MessageEncoder(separators=(',', ':'))
  120. value = encoder.encode(messages)
  121. decoded_messages = json.loads(value, cls=MessageDecoder)
  122. self.assertEqual(messages, decoded_messages)
  123. def test_safedata(self):
  124. """
  125. A message containing SafeData is keeping its safe status when
  126. retrieved from the message storage.
  127. """
  128. def encode_decode(data):
  129. message = Message(constants.DEBUG, data)
  130. encoded = storage._encode(message)
  131. decoded = storage._decode(encoded)
  132. return decoded.message
  133. storage = self.get_storage()
  134. self.assertIsInstance(encode_decode(mark_safe("<b>Hello Django!</b>")), SafeData)
  135. self.assertNotIsInstance(encode_decode("<b>Hello Django!</b>"), SafeData)
  136. def test_pre_1_5_message_format(self):
  137. """
  138. Messages that were set in the cookie before the addition of is_safedata
  139. are decoded correctly (#22426).
  140. """
  141. # Encode the messages using the current encoder.
  142. messages = [Message(constants.INFO, 'message %s') for x in range(5)]
  143. encoder = MessageEncoder(separators=(',', ':'))
  144. encoded_messages = encoder.encode(messages)
  145. # Remove the is_safedata flag from the messages in order to imitate
  146. # the behavior of before 1.5 (monkey patching).
  147. encoded_messages = json.loads(encoded_messages)
  148. for obj in encoded_messages:
  149. obj.pop(1)
  150. encoded_messages = json.dumps(encoded_messages, separators=(',', ':'))
  151. # Decode the messages in the old format (without is_safedata)
  152. decoded_messages = json.loads(encoded_messages, cls=MessageDecoder)
  153. self.assertEqual(messages, decoded_messages)