Test that ANSI input is filtered
[mudpy.git] / mudpy / tests / selftest.py
index 7251b90..0b29386 100644 (file)
@@ -1,7 +1,8 @@
-# Copyright (c) 2004-2017 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# Copyright (c) 2004-2018 Jeremy Stanley <fungi@yuggoth.org>. Permission
 # to use, copy, modify, and distribute this software is granted under
 # terms provided in the LICENSE file distributed with this software.
 
 # to use, copy, modify, and distribute this software is granted under
 # terms provided in the LICENSE file distributed with this software.
 
+import os
 import re
 import sys
 import telnetlib
 import re
 import sys
 import telnetlib
@@ -10,7 +11,7 @@ import time
 test_account0_setup = (
     (0, "Identify yourself:", "luser0"),
     (0, "Enter your choice:", "n"),
 test_account0_setup = (
     (0, "Identify yourself:", "luser0"),
     (0, "Enter your choice:", "n"),
-    (0, "Enter a new password for \"luser0\":", "Test123"),
+    (0, 'Enter a new password for "luser0":', "Test123"),
     (0, "Enter the same new password again:", "Test123"),
     (0, r"What would you like to do\?", "c"),
     (0, "Pick a birth gender for your new avatar:", "f"),
     (0, "Enter the same new password again:", "Test123"),
     (0, r"What would you like to do\?", "c"),
     (0, "Pick a birth gender for your new avatar:", "f"),
@@ -22,7 +23,7 @@ test_account0_setup = (
 test_account1_setup = (
     (1, "Identify yourself:", "luser1"),
     (1, "Enter your choice:", "n"),
 test_account1_setup = (
     (1, "Identify yourself:", "luser1"),
     (1, "Enter your choice:", "n"),
-    (1, "Enter a new password for \"luser1\":", "Test456"),
+    (1, 'Enter a new password for "luser1":', "Test456"),
     (1, "Enter the same new password again:", "Test456"),
     (1, r"What would you like to do\?", "c"),
     (1, "Pick a birth gender for your new avatar:", "m"),
     (1, "Enter the same new password again:", "Test456"),
     (1, r"What would you like to do\?", "c"),
     (1, "Pick a birth gender for your new avatar:", "m"),
@@ -41,18 +42,18 @@ test_explicit_punctuation = (
     (1, r'exclaims, "Hello there\!"', "say And you are?"),
     (1, r'You ask, "And you are\?"', ""),
     (0, r'asks, "And you are\?"', "say I'm me, of course."),
     (1, r'exclaims, "Hello there\!"', "say And you are?"),
     (1, r'You ask, "And you are\?"', ""),
     (0, r'asks, "And you are\?"', "say I'm me, of course."),
-    (0, r'You say, "I\'m me, of course\."', ""),
-    (1, r'says, "I\'m me, of course\."', "say I wouldn't be so sure..."),
-    (1, r'You muse, "I wouldn\'t be so sure\.\.\."', ""),
-    (0, r'muses, "I wouldn\'t be so sure\.\.\."', "say You mean,"),
+    (0, r'''You say, "I'm me, of course\."''', ""),
+    (1, r'''says, "I'm me, of course\."''', "say I wouldn't be so sure..."),
+    (1, r'''You muse, "I wouldn't be so sure\.\.\."''', ""),
+    (0, r'''muses, "I wouldn't be so sure\.\.\."''', "say You mean,"),
     (0, 'You begin, "You mean,"', ""),
     (1, 'begins, "You mean,"', "say I know-"),
     (1, 'You begin, "I know-"', ""),
     (0, 'begins, "I know-"', "say Don't interrupt:"),
     (0, 'You begin, "You mean,"', ""),
     (1, 'begins, "You mean,"', "say I know-"),
     (1, 'You begin, "I know-"', ""),
     (0, 'begins, "I know-"', "say Don't interrupt:"),
-    (0, r'You begin, "Don\'t interrupt:"', ""),
-    (1, r'begins, "Don\'t interrupt:"', "say I wasn't interrupting;"),
-    (1, r'You begin, "I wasn\'t interrupting;"', ""),
-    (0, r'begins, "I wasn\'t interrupting;"', ""),
+    (0, r'''You begin, "Don't interrupt:"''', ""),
+    (1, r'''begins, "Don't interrupt:"''', "say I wasn't interrupting;"),
+    (1, r'''You begin, "I wasn't interrupting;"''', ""),
+    (0, r'''begins, "I wasn't interrupting;"''', ""),
 )
 
 test_implicit_punctuation = (
 )
 
 test_implicit_punctuation = (
@@ -63,20 +64,20 @@ test_implicit_punctuation = (
 
 test_typo_replacement = (
     (1, '> ', "say That's what i think."),
 
 test_typo_replacement = (
     (1, '> ', "say That's what i think."),
-    (1, r'You say, "That\'s what I think\."', ""),
-    (0, r'says, "That\'s what I think\."', "say You know what i'd like."),
-    (0, r'You say, "You know what I\'d like\."', ""),
-    (1, r'says, "You know what I\'d like\."', "say Then i'll tell you."),
-    (1, r'You say, "Then I\'ll tell you\."', ""),
-    (0, r'says, "Then I\'ll tell you\."', "say Now i'm ready."),
-    (0, r'You say, "Now I\'m ready\."', ""),
-    (1, r'says, "Now I\'m ready\."', "say That's teh idea."),
-    (1, r'You say, "That\'s the idea\."', ""),
-    (0, r'says, "That\'s the idea\."', "say It's what theyre saying."),
-    (0, r'You say, "It\'s what they\'re saying\."', ""),
-    (1, r'says, "It\'s what they\'re saying\."', "say Well, youre right."),
-    (1, r'You say, "Well, you\'re right\."', ""),
-    (0, r'says, "Well, you\'re right\."', ""),
+    (1, r'''You say, "That's what I think\."''', ""),
+    (0, r'''says, "That's what I think\."''', "say You know what i'd like."),
+    (0, r'''You say, "You know what I'd like\."''', ""),
+    (1, r'''says, "You know what I'd like\."''', "say Then i'll tell you."),
+    (1, r'''You say, "Then I'll tell you\."''', ""),
+    (0, r'''says, "Then I'll tell you\."''', "say Now i'm ready."),
+    (0, r'''You say, "Now I'm ready\."''', ""),
+    (1, r'''says, "Now I'm ready\."''', "say That's teh idea."),
+    (1, r'''You say, "That's the idea\."''', ""),
+    (0, r'''says, "That's the idea\."''', "say It's what theyre saying."),
+    (0, r'''You say, "It's what they're saying\."''', ""),
+    (1, r'''says, "It's what they're saying\."''', "say Well, youre right."),
+    (1, r'''You say, "Well, you're right\."''', ""),
+    (0, r'''says, "Well, you're right\."''', ""),
 )
 
 test_sentence_capitalization = (
 )
 
 test_sentence_capitalization = (
@@ -95,6 +96,16 @@ test_chat_mode = (
     (0, r'says, "Now less chatty\."', ""),
 )
 
     (0, r'says, "Now less chatty\."', ""),
 )
 
+test_wrapping = (
+    (0, '> ', "say " + 100 * "o"),
+    (1, r'says,\r\n"O[o]+\."', ""),
+)
+
+test_forbid_ansi_input = (
+    (0, '> ', "say \x1b[35mfoo\x1b[0m"),
+    (1, r'says, "\[35mfoo\[0m\."', ""),
+)
+
 test_movement = (
     (0, "> ", "move north"),
     (0, r"You exit to the north\.", ""),
 test_movement = (
     (0, "> ", "move north"),
     (0, r"You exit to the north\.", ""),
@@ -133,7 +144,7 @@ test_account1_teardown = (
 test_admin_setup = (
     (2, "Identify yourself:", "admin"),
     (2, "Enter your choice:", "n"),
 test_admin_setup = (
     (2, "Identify yourself:", "admin"),
     (2, "Enter your choice:", "n"),
-    (2, "Enter a new password for \"admin\":", "Test789"),
+    (2, 'Enter a new password for "admin":', "Test789"),
     (2, "Enter the same new password again:", "Test789"),
     (2, r"What would you like to do\?", "c"),
     (2, "Pick a birth gender for your new avatar:", "m"),
     (2, "Enter the same new password again:", "Test789"),
     (2, r"What would you like to do\?", "c"),
     (2, "Pick a birth gender for your new avatar:", "m"),
@@ -142,6 +153,21 @@ test_admin_setup = (
     (2, "Whom would you like to awaken?", ""),
 )
 
     (2, "Whom would you like to awaken?", ""),
 )
 
+test_telnet_iac = (
+    # Send a double (escaped) IAC byte within other text, which should get
+    # unescaped and deduplicated to a single \xff in the buffer and then
+    # the line of input discarded as a non-ASCII sequence
+    (2, "> ", b"say argle\xff\xffbargle\r\n"),
+    (2, r"Non-ASCII characters from admin: b'say argle\\xffbargle'.*> ", ""),
+)
+
+test_telnet_unknown = (
+    # Send an unsupported negotiation command #127 which should get filtered
+    # from the line of input
+    (2, "> ", b"say glop\xff\x7fglyf\r\n"),
+    (2, r'Unknown Telnet IAC command 127 ignored\..*"Glopglyf\.".*> ', ""),
+)
+
 test_admin_restriction = (
     (0, "> ", "help halt"),
     (0, r"That is not an available command\.", "halt"),
 test_admin_restriction = (
     (0, "> ", "help halt"),
     (0, r"That is not an available command\.", "halt"),
@@ -154,11 +180,59 @@ test_admin_help = (
     (2, "This will save all active accounts", ""),
 )
 
     (2, "This will save all active accounts", ""),
 )
 
+test_reload = (
+    (2, "> ", "reload"),
+    (2, r"Reloading all code modules, configs and data\."
+        r".* User admin reloaded the world\.", ""),
+)
+
+test_set_facet = (
+    (2, "> ", "set actor.avatar_admin_0 gender female"),
+    (2, r'You have successfully \(re\)set the "gender" facet of element', ""),
+)
+
+test_set_refused = (
+    (2, "> ", "set mudpy.limit password_tries 10"),
+    (2, r'The "mudpy\.limit" element is kept in read-only file', ""),
+)
+
+test_show_version = (
+    (2, "> ", "show version"),
+    (2, r"Running mudpy .* on .* Python 3.*with.*pyyaml.*> ", ""),
+)
+
+test_show_files = (
+    (2, "> ", "show files"),
+    (2, r'These are the current files containing the universe:.*'
+        r'  \x1b\[31m\(rw\) \x1b\[32m/.*/account.yaml\x1b\[0m'
+        r' \x1b\[33m\[private\]\x1b\[0m.*> ', ""),
+)
+
+test_show_file = (
+    (2, "> ", "show file %s" %
+        os.path.join(os.getcwd(), "data/internal.yaml")),
+    (2, r'These are the nodes in the.*file:.*internal\.counters.*> ', ""),
+)
+
+test_show_groups = (
+    (2, "> ", "show groups"),
+    (2, r'These are the element groups:.*'
+        r'  \x1b\[32maccount\x1b\[0m.*> ', ""),
+)
+
+test_show_group = (
+    (2, "> ", "show group account"),
+    (2, r'These are the elements in the "account" group:.*'
+        r'  \x1b\[32maccount\.admin\x1b\[0m.*> ', ""),
+)
+
 test_show_element = (
 test_show_element = (
-    (2, "> ", "show element internal:counters"),
-    (2, r'These are the properties of the "internal:counters" element '
-        r'\(in.*data/internal\.yaml"\):.*   \x1b\[32melapsed: '
-        r'\x1b\[31m[0-9]+\x1b\[0m', ""),
+    (2, "> ", "show element mudpy.limit"),
+    (2, r'These are the properties of the "mudpy\.limit" element.*'
+        r'  \x1b\[32mpassword_tries: \x1b\[31m3.*> ',
+     "show element actor.avatar_admin_0"),
+    (2, r'These are the properties of the "actor.avatar_admin_0" element.*'
+        r'  \x1b\[32mgender: \x1b\[31mfemale.*> ', ""),
 )
 
 test_show_log = (
 )
 
 test_show_log = (
@@ -168,15 +242,15 @@ test_show_log = (
 )
 
 test_custom_loglevel = (
 )
 
 test_custom_loglevel = (
-    (2, "> ", "set account:admin loglevel 2"),
+    (2, "> ", "set account.admin loglevel 2"),
     (2, "You have successfully .*> ", "show log"),
     (2, r"There are [0-9]+ log lines in memory and [0-9]+ at or above level "
         r"[0-9]+\. The matching lines\r\nfrom [0-9]+ to [0-9]+ are:", ""),
 )
 
 test_invalid_loglevel = (
     (2, "You have successfully .*> ", "show log"),
     (2, r"There are [0-9]+ log lines in memory and [0-9]+ at or above level "
         r"[0-9]+\. The matching lines\r\nfrom [0-9]+ to [0-9]+ are:", ""),
 )
 
 test_invalid_loglevel = (
-    (2, "> ", "set account:admin loglevel two"),
-    (2, r'Value "two" of type "<class \'str\'>" cannot be coerced .*> ', ""),
+    (2, "> ", "set account.admin loglevel two"),
+    (2, r'''Value "two" of type "<class 'str'>" cannot be coerced .*> ''', ""),
 )
 
 test_log_no_errors = (
 )
 
 test_log_no_errors = (
@@ -193,12 +267,24 @@ dialogue = (
     (test_typo_replacement, "typo replacement"),
     (test_sentence_capitalization, "sentence capitalization"),
     (test_chat_mode, "chat mode"),
     (test_typo_replacement, "typo replacement"),
     (test_sentence_capitalization, "sentence capitalization"),
     (test_chat_mode, "chat mode"),
+    (test_wrapping, "wrapping"),
+    (test_forbid_ansi_input, "raw escape input is filtered"),
     (test_movement, "movement"),
     (test_actor_disappears, "actor spontaneous disappearance"),
     (test_account1_teardown, "second account teardown"),
     (test_admin_setup, "admin account setup"),
     (test_movement, "movement"),
     (test_actor_disappears, "actor spontaneous disappearance"),
     (test_account1_teardown, "second account teardown"),
     (test_admin_setup, "admin account setup"),
+    (test_telnet_iac, "escape stray telnet iac bytes"),
+    (test_telnet_unknown, "strip unknown telnet command"),
     (test_admin_restriction, "restricted admin commands"),
     (test_admin_help, "admin help"),
     (test_admin_restriction, "restricted admin commands"),
     (test_admin_help, "admin help"),
+    (test_reload, "reload"),
+    (test_set_facet, "set facet"),
+    (test_set_refused, "refuse altering read-only element"),
+    (test_show_version, "show version and diagnostic info"),
+    (test_show_files, "show a list of loaded files"),
+    (test_show_file, "show nodes from a specific file"),
+    (test_show_groups, "show groups"),
+    (test_show_group, "show group"),
     (test_show_element, "show element"),
     (test_show_log, "show log"),
     (test_custom_loglevel, "custom loglevel"),
     (test_show_element, "show element"),
     (test_show_log, "show log"),
     (test_custom_loglevel, "custom loglevel"),
@@ -213,19 +299,31 @@ def main():
     success = True
     start = time.time()
     for luser in lusers:
     success = True
     start = time.time()
     for luser in lusers:
-        luser.open("::1", 6669)
+        luser.open("::1", 4000)
     for test, description in dialogue:
         print("\nTesting %s..." % description)
         test_start = time.time()
         for conversant, question, answer in test:
             print("luser%s waiting for: %s" % (conversant, question))
     for test, description in dialogue:
         print("\nTesting %s..." % description)
         test_start = time.time()
         for conversant, question, answer in test:
             print("luser%s waiting for: %s" % (conversant, question))
-            index, match, received = lusers[conversant].expect(
-                [re.compile(question.encode("utf-8"), flags=re.DOTALL)], 5)
-            captures[conversant] += received.decode("utf-8")
+            try:
+                index, match, received = lusers[conversant].expect(
+                    [re.compile(question.encode("utf-8"), flags=re.DOTALL)], 5)
+                captures[conversant] += received.decode("utf-8")
+            except ConnectionResetError:
+                print("ERROR: Unable to connect to server.")
+                success = False
+                break
+            except EOFError:
+                print("ERROR: luser%s premature disconnection expecting:\n\n"
+                      "%s\n\n"
+                      "Check the end of capture_%s.log for received data."
+                      % (conversant, question, conversant))
+                success = False
+                break
             try:
                 captures[conversant] += lusers[
                     conversant].read_very_eager().decode("utf-8")
             try:
                 captures[conversant] += lusers[
                     conversant].read_very_eager().decode("utf-8")
-            except:
+            except Exception:
                 pass
             if index is not 0:
                 print("ERROR: luser%s did not receive expected string:\n\n"
                 pass
             if index is not 0:
                 print("ERROR: luser%s did not receive expected string:\n\n"
@@ -234,9 +332,19 @@ def main():
                       % (conversant, question, conversant))
                 success = False
                 break
                       % (conversant, question, conversant))
                 success = False
                 break
-            print("luser%s sending: %s" % (conversant, answer))
-            lusers[conversant].write(("%s\r\n" % answer).encode("utf-8"))
-            captures[conversant] += "%s\r\n" % answer
+            if type(answer) is str:
+                print("luser%s sending: %s" % (conversant, answer))
+                lusers[conversant].write(("%s\r\n" % answer).encode("utf-8"))
+                captures[conversant] += "%s\r\n" % answer
+            elif type(answer) is bytes:
+                print("luser%s sending raw bytes: %s" % (conversant, answer))
+                lusers[conversant].get_socket().send(answer)
+                captures[conversant] += "!!!RAW BYTES: %s" % answer
+            else:
+                print("ERROR: answer provided with unsupported type %s"
+                      % type(answer))
+                success = False
+                break
         if not success:
             break
         print("Completed in %.3f seconds." % (time.time() - test_start))
         if not success:
             break
         print("Completed in %.3f seconds." % (time.time() - test_start))
@@ -246,7 +354,7 @@ def main():
         try:
             captures[conversant] += lusers[
                 conversant].read_very_eager().decode("utf-8")
         try:
             captures[conversant] += lusers[
                 conversant].read_very_eager().decode("utf-8")
-        except:
+        except Exception:
             pass
         lusers[conversant].close()
         logfile = "capture_%s.log" % conversant
             pass
         lusers[conversant].close()
         logfile = "capture_%s.log" % conversant