@@ -94,7 +94,6 @@ def test_django_logger_debug(self):
9494
9595
9696class LoggingAssertionMixin :
97-
9897 def assertLogRecord (
9998 self ,
10099 logger_cm ,
@@ -147,6 +146,14 @@ def test_page_not_found_warning(self):
147146 msg = "Not Found: /does_not_exist/" ,
148147 )
149148
149+ def test_control_chars_escaped (self ):
150+ self .assertLogsRequest (
151+ url = "/%1B[1;31mNOW IN RED!!!1B[0m/" ,
152+ level = "WARNING" ,
153+ status_code = 404 ,
154+ msg = r"Not Found: /\x1b[1;31mNOW IN RED!!!1B[0m/" ,
155+ )
156+
150157 async def test_async_page_not_found_warning (self ):
151158 logger = "django.request"
152159 level = "WARNING"
@@ -155,6 +162,16 @@ async def test_async_page_not_found_warning(self):
155162
156163 self .assertLogRecord (cm , level , "Not Found: /does_not_exist/" , 404 )
157164
165+ async def test_async_control_chars_escaped (self ):
166+ logger = "django.request"
167+ level = "WARNING"
168+ with self .assertLogs (logger , level ) as cm :
169+ await self .async_client .get (r"/%1B[1;31mNOW IN RED!!!1B[0m/" )
170+
171+ self .assertLogRecord (
172+ cm , level , r"Not Found: /\x1b[1;31mNOW IN RED!!!1B[0m/" , 404
173+ )
174+
158175 def test_page_not_found_raised (self ):
159176 self .assertLogsRequest (
160177 url = "/does_not_exist_raised/" ,
@@ -686,6 +703,7 @@ def assertResponseLogged(self, logger_cm, msg, levelno, status_code, request):
686703 self .assertEqual (record .levelno , levelno )
687704 self .assertEqual (record .status_code , status_code )
688705 self .assertEqual (record .request , request )
706+ return record
689707
690708 def test_missing_response_raises_attribute_error (self ):
691709 with self .assertRaises (AttributeError ):
@@ -787,3 +805,62 @@ def test_logs_with_custom_logger(self):
787805 self .assertEqual (
788806 f"WARNING:my.custom.logger:{ msg } " , log_stream .getvalue ().strip ()
789807 )
808+
809+ def test_unicode_escape_escaping (self ):
810+ test_cases = [
811+ # Control characters.
812+ ("line\n break" , "line\\ nbreak" ),
813+ ("carriage\r return" , "carriage\\ rreturn" ),
814+ ("tab\t separated" , "tab\\ tseparated" ),
815+ ("formfeed\f " , "formfeed\\ x0c" ),
816+ ("bell\a " , "bell\\ x07" ),
817+ ("multi\n line\n text" , "multi\\ nline\\ ntext" ),
818+ # Slashes.
819+ ("slash\\ test" , "slash\\ \\ test" ),
820+ ("back\\ slash" , "back\\ \\ slash" ),
821+ # Quotes.
822+ ('quote"test"' , 'quote"test"' ),
823+ ("quote'test'" , "quote'test'" ),
824+ # Accented, composed characters, emojis and symbols.
825+ ("café" , "caf\\ xe9" ),
826+ ("e\u0301 " , "e\\ u0301" ), # e + combining acute
827+ ("smile🙂" , "smile\\ U0001f642" ),
828+ ("weird ☃️" , "weird \\ u2603\\ ufe0f" ),
829+ # Non-Latin alphabets.
830+ ("Привет" , "\\ u041f\\ u0440\\ u0438\\ u0432\\ u0435\\ u0442" ),
831+ ("你好" , "\\ u4f60\\ u597d" ),
832+ # ANSI escape sequences.
833+ ("escape\x1b [31mred\x1b [0m" , "escape\\ x1b[31mred\\ x1b[0m" ),
834+ (
835+ "/\x1b [1;31mCAUTION!!YOU ARE PWNED\x1b [0m/" ,
836+ "/\\ x1b[1;31mCAUTION!!YOU ARE PWNED\\ x1b[0m/" ,
837+ ),
838+ (
839+ "/\r \n \r \n 1984-04-22 INFO Listening on 0.0.0.0:8080\r \n \r \n " ,
840+ "/\\ r\\ n\\ r\\ n1984-04-22 INFO Listening on 0.0.0.0:8080\\ r\\ n\\ r\\ n" ,
841+ ),
842+ # Plain safe input.
843+ ("normal-path" , "normal-path" ),
844+ ("slash/colon:" , "slash/colon:" ),
845+ # Non strings.
846+ (0 , "0" ),
847+ ([1 , 2 , 3 ], "[1, 2, 3]" ),
848+ ({"test" : "🙂" }, "{'test': '🙂'}" ),
849+ ]
850+
851+ msg = "Test message: %s"
852+ for case , expected in test_cases :
853+ with self .assertLogs ("django.request" , level = "ERROR" ) as cm :
854+ with self .subTest (case = case ):
855+ response = HttpResponse (status = 318 )
856+ log_response (msg , case , response = response , level = "error" )
857+
858+ record = self .assertResponseLogged (
859+ cm ,
860+ msg % expected ,
861+ levelno = logging .ERROR ,
862+ status_code = 318 ,
863+ request = None ,
864+ )
865+ # Log record is always a single line.
866+ self .assertEqual (len (record .getMessage ().splitlines ()), 1 )
0 commit comments