1. import json
    
  2. import random
    
  3. 
    
  4. from django.conf import settings
    
  5. from django.contrib.messages import constants
    
  6. from django.contrib.messages.storage.base import Message
    
  7. from django.contrib.messages.storage.cookie import (
    
  8.     CookieStorage,
    
  9.     MessageDecoder,
    
  10.     MessageEncoder,
    
  11. )
    
  12. from django.test import SimpleTestCase, override_settings
    
  13. from django.utils.crypto import get_random_string
    
  14. from django.utils.safestring import SafeData, mark_safe
    
  15. 
    
  16. from .base import BaseTests
    
  17. 
    
  18. 
    
  19. def set_cookie_data(storage, messages, invalid=False, encode_empty=False):
    
  20.     """
    
  21.     Set ``request.COOKIES`` with the encoded data and remove the storage
    
  22.     backend's loaded data cache.
    
  23.     """
    
  24.     encoded_data = storage._encode(messages, encode_empty=encode_empty)
    
  25.     if invalid:
    
  26.         # Truncate the first character so that the hash is invalid.
    
  27.         encoded_data = encoded_data[1:]
    
  28.     storage.request.COOKIES = {CookieStorage.cookie_name: encoded_data}
    
  29.     if hasattr(storage, "_loaded_data"):
    
  30.         del storage._loaded_data
    
  31. 
    
  32. 
    
  33. def stored_cookie_messages_count(storage, response):
    
  34.     """
    
  35.     Return an integer containing the number of messages stored.
    
  36.     """
    
  37.     # Get a list of cookies, excluding ones with a max-age of 0 (because
    
  38.     # they have been marked for deletion).
    
  39.     cookie = response.cookies.get(storage.cookie_name)
    
  40.     if not cookie or cookie["max-age"] == 0:
    
  41.         return 0
    
  42.     data = storage._decode(cookie.value)
    
  43.     if not data:
    
  44.         return 0
    
  45.     if data[-1] == CookieStorage.not_finished:
    
  46.         data.pop()
    
  47.     return len(data)
    
  48. 
    
  49. 
    
  50. @override_settings(
    
  51.     SESSION_COOKIE_DOMAIN=".example.com",
    
  52.     SESSION_COOKIE_SECURE=True,
    
  53.     SESSION_COOKIE_HTTPONLY=True,
    
  54. )
    
  55. class CookieTests(BaseTests, SimpleTestCase):
    
  56.     storage_class = CookieStorage
    
  57. 
    
  58.     def stored_messages_count(self, storage, response):
    
  59.         return stored_cookie_messages_count(storage, response)
    
  60. 
    
  61.     def encode_decode(self, *args, **kwargs):
    
  62.         storage = self.get_storage()
    
  63.         message = Message(constants.DEBUG, *args, **kwargs)
    
  64.         encoded = storage._encode(message)
    
  65.         return storage._decode(encoded)
    
  66. 
    
  67.     def test_get(self):
    
  68.         storage = self.storage_class(self.get_request())
    
  69.         # Set initial data.
    
  70.         example_messages = ["test", "me"]
    
  71.         set_cookie_data(storage, example_messages)
    
  72.         # The message contains what's expected.
    
  73.         self.assertEqual(list(storage), example_messages)
    
  74. 
    
  75.     @override_settings(SESSION_COOKIE_SAMESITE="Strict")
    
  76.     def test_cookie_setings(self):
    
  77.         """
    
  78.         CookieStorage honors SESSION_COOKIE_DOMAIN, SESSION_COOKIE_SECURE, and
    
  79.         SESSION_COOKIE_HTTPONLY (#15618, #20972).
    
  80.         """
    
  81.         # Test before the messages have been consumed
    
  82.         storage = self.get_storage()
    
  83.         response = self.get_response()
    
  84.         storage.add(constants.INFO, "test")
    
  85.         storage.update(response)
    
  86.         messages = storage._decode(response.cookies["messages"].value)
    
  87.         self.assertEqual(len(messages), 1)
    
  88.         self.assertEqual(messages[0].message, "test")
    
  89.         self.assertEqual(response.cookies["messages"]["domain"], ".example.com")
    
  90.         self.assertEqual(response.cookies["messages"]["expires"], "")
    
  91.         self.assertIs(response.cookies["messages"]["secure"], True)
    
  92.         self.assertIs(response.cookies["messages"]["httponly"], True)
    
  93.         self.assertEqual(response.cookies["messages"]["samesite"], "Strict")
    
  94. 
    
  95.         # Deletion of the cookie (storing with an empty value) after the
    
  96.         # messages have been consumed.
    
  97.         storage = self.get_storage()
    
  98.         response = self.get_response()
    
  99.         storage.add(constants.INFO, "test")
    
  100.         for m in storage:
    
  101.             pass  # Iterate through the storage to simulate consumption of messages.
    
  102.         storage.update(response)
    
  103.         self.assertEqual(response.cookies["messages"].value, "")
    
  104.         self.assertEqual(response.cookies["messages"]["domain"], ".example.com")
    
  105.         self.assertEqual(
    
  106.             response.cookies["messages"]["expires"], "Thu, 01 Jan 1970 00:00:00 GMT"
    
  107.         )
    
  108.         self.assertEqual(
    
  109.             response.cookies["messages"]["samesite"],
    
  110.             settings.SESSION_COOKIE_SAMESITE,
    
  111.         )
    
  112. 
    
  113.     def test_get_bad_cookie(self):
    
  114.         request = self.get_request()
    
  115.         storage = self.storage_class(request)
    
  116.         # Set initial (invalid) data.
    
  117.         example_messages = ["test", "me"]
    
  118.         set_cookie_data(storage, example_messages, invalid=True)
    
  119.         # The message actually contains what we expect.
    
  120.         self.assertEqual(list(storage), [])
    
  121. 
    
  122.     def test_max_cookie_length(self):
    
  123.         """
    
  124.         If the data exceeds what is allowed in a cookie, older messages are
    
  125.         removed before saving (and returned by the ``update`` method).
    
  126.         """
    
  127.         storage = self.get_storage()
    
  128.         response = self.get_response()
    
  129. 
    
  130.         # When storing as a cookie, the cookie has constant overhead of approx
    
  131.         # 54 chars, and each message has a constant overhead of about 37 chars
    
  132.         # and a variable overhead of zero in the best case. We aim for a message
    
  133.         # size which will fit 4 messages into the cookie, but not 5.
    
  134.         # See also FallbackTest.test_session_fallback
    
  135.         msg_size = int((CookieStorage.max_cookie_size - 54) / 4.5 - 37)
    
  136.         first_msg = None
    
  137.         # Generate the same (tested) content every time that does not get run
    
  138.         # through zlib compression.
    
  139.         random.seed(42)
    
  140.         for i in range(5):
    
  141.             msg = get_random_string(msg_size)
    
  142.             storage.add(constants.INFO, msg)
    
  143.             if i == 0:
    
  144.                 first_msg = msg
    
  145.         unstored_messages = storage.update(response)
    
  146. 
    
  147.         cookie_storing = self.stored_messages_count(storage, response)
    
  148.         self.assertEqual(cookie_storing, 4)
    
  149. 
    
  150.         self.assertEqual(len(unstored_messages), 1)
    
  151.         self.assertEqual(unstored_messages[0].message, first_msg)
    
  152. 
    
  153.     def test_message_rfc6265(self):
    
  154.         non_compliant_chars = ["\\", ",", ";", '"']
    
  155.         messages = ["\\te,st", ';m"e', "\u2019", '123"NOTRECEIVED"']
    
  156.         storage = self.get_storage()
    
  157.         encoded = storage._encode(messages)
    
  158.         for illegal in non_compliant_chars:
    
  159.             self.assertEqual(encoded.find(illegal), -1)
    
  160. 
    
  161.     def test_json_encoder_decoder(self):
    
  162.         """
    
  163.         A complex nested data structure containing Message
    
  164.         instances is properly encoded/decoded by the custom JSON
    
  165.         encoder/decoder classes.
    
  166.         """
    
  167.         messages = [
    
  168.             {
    
  169.                 "message": Message(constants.INFO, "Test message"),
    
  170.                 "message_list": [
    
  171.                     Message(constants.INFO, "message %s") for x in range(5)
    
  172.                 ]
    
  173.                 + [{"another-message": Message(constants.ERROR, "error")}],
    
  174.             },
    
  175.             Message(constants.INFO, "message %s"),
    
  176.         ]
    
  177.         encoder = MessageEncoder()
    
  178.         value = encoder.encode(messages)
    
  179.         decoded_messages = json.loads(value, cls=MessageDecoder)
    
  180.         self.assertEqual(messages, decoded_messages)
    
  181. 
    
  182.     def test_safedata(self):
    
  183.         """
    
  184.         A message containing SafeData is keeping its safe status when
    
  185.         retrieved from the message storage.
    
  186.         """
    
  187.         self.assertIsInstance(
    
  188.             self.encode_decode(mark_safe("<b>Hello Django!</b>")).message,
    
  189.             SafeData,
    
  190.         )
    
  191.         self.assertNotIsInstance(
    
  192.             self.encode_decode("<b>Hello Django!</b>").message,
    
  193.             SafeData,
    
  194.         )
    
  195. 
    
  196.     def test_extra_tags(self):
    
  197.         """
    
  198.         A message's extra_tags attribute is correctly preserved when retrieved
    
  199.         from the message storage.
    
  200.         """
    
  201.         for extra_tags in ["", None, "some tags"]:
    
  202.             with self.subTest(extra_tags=extra_tags):
    
  203.                 self.assertEqual(
    
  204.                     self.encode_decode("message", extra_tags=extra_tags).extra_tags,
    
  205.                     extra_tags,
    
  206.                 )