2
0

test_cookie.py 7.6 KB

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