@@ -147,6 +147,14 @@ def test_page_not_found_warning(self):
147
147
msg = "Not Found: /does_not_exist/" ,
148
148
)
149
149
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
+
150
158
async def test_async_page_not_found_warning (self ):
151
159
logger = "django.request"
152
160
level = "WARNING"
@@ -155,6 +163,16 @@ async def test_async_page_not_found_warning(self):
155
163
156
164
self .assertLogRecord (cm , level , "Not Found: /does_not_exist/" , 404 )
157
165
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
+
158
176
def test_page_not_found_raised (self ):
159
177
self .assertLogsRequest (
160
178
url = "/does_not_exist_raised/" ,
@@ -705,6 +723,7 @@ def assertResponseLogged(self, logger_cm, msg, levelno, status_code, request):
705
723
self .assertEqual (record .levelno , levelno )
706
724
self .assertEqual (record .status_code , status_code )
707
725
self .assertEqual (record .request , request )
726
+ return record
708
727
709
728
def test_missing_response_raises_attribute_error (self ):
710
729
with self .assertRaises (AttributeError ):
@@ -806,3 +825,64 @@ def test_logs_with_custom_logger(self):
806
825
self .assertEqual (
807
826
f"WARNING:my.custom.logger:{ msg } " , log_stream .getvalue ().strip ()
808
827
)
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