@@ -147,6 +147,14 @@ def test_page_not_found_warning(self):
147147 msg = "Not Found: /does_not_exist/" ,
148148 )
149149
150+ def test_control_chars_escaped (self ):
151+ self .assertLogsRequest (
152+ url = "/%1B[1;31mNOW IN RED!!!1B[0m/" ,
153+ level = "WARNING" ,
154+ status_code = 404 ,
155+ msg = r"Not Found: /\x1b[1;31mNOW IN RED!!!1B[0m/" ,
156+ )
157+
150158 async def test_async_page_not_found_warning (self ):
151159 logger = "django.request"
152160 level = "WARNING"
@@ -155,6 +163,16 @@ async def test_async_page_not_found_warning(self):
155163
156164 self .assertLogRecord (cm , level , "Not Found: /does_not_exist/" , 404 )
157165
166+ async def test_async_control_chars_escaped (self ):
167+ logger = "django.request"
168+ level = "WARNING"
169+ with self .assertLogs (logger , level ) as cm :
170+ await self .async_client .get (r"/%1B[1;31mNOW IN RED!!!1B[0m/" )
171+
172+ self .assertLogRecord (
173+ cm , level , r"Not Found: /\x1b[1;31mNOW IN RED!!!1B[0m/" , 404
174+ )
175+
158176 def test_page_not_found_raised (self ):
159177 self .assertLogsRequest (
160178 url = "/does_not_exist_raised/" ,
@@ -705,6 +723,7 @@ def assertResponseLogged(self, logger_cm, msg, levelno, status_code, request):
705723 self .assertEqual (record .levelno , levelno )
706724 self .assertEqual (record .status_code , status_code )
707725 self .assertEqual (record .request , request )
726+ return record
708727
709728 def test_missing_response_raises_attribute_error (self ):
710729 with self .assertRaises (AttributeError ):
@@ -806,3 +825,64 @@ def test_logs_with_custom_logger(self):
806825 self .assertEqual (
807826 f"WARNING:my.custom.logger:{ msg } " , log_stream .getvalue ().strip ()
808827 )
828+
829+ def test_unicode_escape_escaping (self ):
830+ test_cases = [
831+ # Control characters.
832+ ("line\n break" , "line\\ nbreak" ),
833+ ("carriage\r return" , "carriage\\ rreturn" ),
834+ ("tab\t separated" , "tab\\ tseparated" ),
835+ ("formfeed\f " , "formfeed\\ x0c" ),
836+ ("bell\a " , "bell\\ x07" ),
837+ ("multi\n line\n text" , "multi\\ nline\\ ntext" ),
838+ # Slashes.
839+ ("slash\\ test" , "slash\\ \\ test" ),
840+ ("back\\ slash" , "back\\ \\ slash" ),
841+ # Quotes.
842+ ('quote"test"' , 'quote"test"' ),
843+ ("quote'test'" , "quote'test'" ),
844+ # Accented, composed characters, emojis and symbols.
845+ ("café" , "caf\\ xe9" ),
846+ ("e\u0301 " , "e\\ u0301" ), # e + combining acute
847+ ("smile🙂" , "smile\\ U0001f642" ),
848+ ("weird ☃️" , "weird \\ u2603\\ ufe0f" ),
849+ # Non-Latin alphabets.
850+ ("Привет" , "\\ u041f\\ u0440\\ u0438\\ u0432\\ u0435\\ u0442" ),
851+ ("你好" , "\\ u4f60\\ u597d" ),
852+ # ANSI escape sequences.
853+ ("escape\x1b [31mred\x1b [0m" , "escape\\ x1b[31mred\\ x1b[0m" ),
854+ (
855+ "/\x1b [1;31mCAUTION!!YOU ARE PWNED\x1b [0m/" ,
856+ "/\\ x1b[1;31mCAUTION!!YOU ARE PWNED\\ x1b[0m/" ,
857+ ),
858+ (
859+ "/\r \n \r \n 1984-04-22 INFO Listening on 0.0.0.0:8080\r \n \r \n " ,
860+ "/\\ r\\ n\\ r\\ n1984-04-22 INFO Listening on 0.0.0.0:8080\\ r\\ n\\ r\\ n" ,
861+ ),
862+ # Plain safe input.
863+ ("normal-path" , "normal-path" ),
864+ ("slash/colon:" , "slash/colon:" ),
865+ # Non strings.
866+ (0 , "0" ),
867+ ([1 , 2 , 3 ], "[1, 2, 3]" ),
868+ ({"test" : "🙂" }, "{'test': '🙂'}" ),
869+ ]
870+
871+ msg = "Test message: %s"
872+ for case , expected in test_cases :
873+ with (
874+ self .assertLogs ("django.request" , level = "ERROR" ) as cm ,
875+ self .subTest (case = case ),
876+ ):
877+ response = HttpResponse (status = 318 )
878+ log_response (msg , case , response = response , level = "error" )
879+
880+ record = self .assertResponseLogged (
881+ cm ,
882+ msg % expected ,
883+ levelno = logging .ERROR ,
884+ status_code = 318 ,
885+ request = None ,
886+ )
887+ # Log record is always a single line.
888+ self .assertEqual (len (record .getMessage ().splitlines ()), 1 )
0 commit comments