From 6c393c4e00d46569e113bcd29cf6f7ef8908ff29 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 23 Dec 2021 17:15:10 +0100 Subject: [PATCH] Add tests for WHO (#122) * Add tests for WHO * Make the mask in RPL_ENDOFWHO case-insensitive + skip test when there is a space in the mask * Remove 'o' flag of WHO, it's not consistently implemented * Skip matches on username and realname (for now?) * Add workarounds from irc2 and ircu2. * Add test for 'WHO *'. * Exclude mask tests in test_who.py for Bahamut --- Makefile | 5 + irctest/patma.py | 11 ++ irctest/server_tests/who.py | 325 ++++++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 irctest/server_tests/who.py diff --git a/Makefile b/Makefile index 6a7e9e2..ad60aba 100644 --- a/Makefile +++ b/Makefile @@ -12,12 +12,15 @@ ANOPE_SELECTORS := \ and not testPlainLarge # buffering tests cannot pass because of issues with UTF-8 handling: https://github.com/DALnet/bahamut/issues/196 +# mask tests in test_who.py fail because they are not implemented. BAHAMUT_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ and not IRCv3 \ and not buffering \ + and not (testWho and not whois and mask) \ + and not testWhoStar \ $(EXTRA_SELECTORS) # testQuitErrors is very flaky @@ -159,6 +162,7 @@ SOPEL_SELECTORS := \ # Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs # testChathistory[BETWEEN] fails: https://bugs.unrealircd.org/view.php?id=5952 # testChathistory[AROUND] fails: https://bugs.unrealircd.org/view.php?id=5953 +# testWhoAllOpers fails because Unreal skips results when the mask is too broad UNREALIRCD_SELECTORS := \ not Ergo \ and not deprecated \ @@ -172,6 +176,7 @@ UNREALIRCD_SELECTORS := \ and not react_tag \ and not private_chathistory \ and not (testChathistory and (between or around)) \ + and not testWhoAllOpers \ $(EXTRA_SELECTORS) .PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd diff --git a/irctest/patma.py b/irctest/patma.py index fd9db2b..1717417 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -43,6 +43,14 @@ class NotStrRe(Operator): return f"NotStrRe(r'{self.regexp}')" +@dataclasses.dataclass(frozen=True) +class InsensitiveStr(Operator): + string: str + + def __repr__(self) -> str: + return f"InsensitiveStr({self.string!r})" + + @dataclasses.dataclass(frozen=True) class RemainingKeys(Operator): """Used in a dict pattern to match all remaining keys. @@ -82,6 +90,9 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo elif isinstance(expected, NotStrRe): if got is None or re.match(expected.regexp, got): return False + elif isinstance(expected, InsensitiveStr): + if got is None or got.lower() != expected.string.lower(): + return False elif isinstance(expected, Operator): raise NotImplementedError(f"Unsupported operator: {expected}") elif got != expected: diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py new file mode 100644 index 0000000..bfb8cb4 --- /dev/null +++ b/irctest/server_tests/who.py @@ -0,0 +1,325 @@ +import re + +import pytest + +from irctest import cases +from irctest.numerics import RPL_ENDOFWHO, RPL_WHOREPLY, RPL_YOUREOPER +from irctest.patma import ANYSTR, InsensitiveStr, StrRe + + +def realname_regexp(realname): + return ( + "[0-9]+ " # is 0 for every IRCd I can find, except ircu2 (which returns 3) + + "(0042 )?" # for irc2... + + re.escape(realname) + ) + + +class WhoTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): + def _init(self): + self.nick = "coolNick" + self.username = "myusernam" # may be truncated if longer than this + self.realname = "My UniqueReal Name" + + self.addClient() + self.sendLine(1, f"NICK {self.nick}") + self.sendLine(1, f"USER {self.username} 0 * :{self.realname}") + self.skipToWelcome(1) + self.sendLine(1, "JOIN #chan") + + self.getMessages(1) + + self.connectClient("otherNick") + self.getMessages(2) + self.sendLine(2, "JOIN #chan") + self.getMessages(2) + + def _checkReply(self, reply, flags): + host_re = "[0-9A-Za-z_:.-]+" + if reply.params[1] == "*": + # Unreal, ... + self.assertMessageMatch( + reply, + command=RPL_WHOREPLY, + params=[ + "otherNick", + "*", # no chan + StrRe("~?" + self.username), + StrRe(host_re), + "My.Little.Server", + "coolNick", + flags, + StrRe(realname_regexp(self.realname)), + ], + ) + else: + # Solanum, Insp, ... + self.assertMessageMatch( + reply, + command=RPL_WHOREPLY, + params=[ + "otherNick", + "#chan", + StrRe("~?" + self.username), + StrRe(host_re), + "My.Little.Server", + "coolNick", + flags + "@", + StrRe(realname_regexp(self.realname)), + ], + ) + + @cases.mark_specifications("Modern") + def testWhoStar(self): + self._init() + + self.sendLine(2, "WHO *") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 3, "Unexpected number of messages") + + (*replies, end) = messages + + # Get them in deterministic order + replies.sort(key=lambda msg: msg.params[5]) + + self._checkReply(replies[0], "H") + + # " `` MUST be exactly the `` parameter sent by the client + # in its `WHO` message. This means the case MUST be preserved." + # -- https://github.com/ircdocs/modern-irc/pull/138/files + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", "*", ANYSTR], + ) + + @pytest.mark.parametrize( + "mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"] + ) + @cases.mark_specifications("Modern") + def testWhoNick(self, mask): + self._init() + + self.sendLine(2, f"WHO {mask}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self._checkReply(reply, "H") + + # " `` MUST be exactly the `` parameter sent by the client + # in its `WHO` message. This means the case MUST be preserved." + # -- https://github.com/ircdocs/modern-irc/pull/138/files + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr(mask), ANYSTR], + ) + + @pytest.mark.skip("Not consistently implemented") + @pytest.mark.parametrize( + "mask", + ["*usernam", "*UniqueReal*", "127.0.0.1"], + ids=["username", "realname-mask", "hostname"], + ) + def testWhoUsernameRealName(self, mask): + self._init() + + self.sendLine(2, f"WHO :{mask}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self._checkReply(reply, "H") + + # " `` MUST be exactly the `` parameter sent by the client + # in its `WHO` message. This means the case MUST be preserved." + # -- https://github.com/ircdocs/modern-irc/pull/138/files + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr(mask), ANYSTR], + ) + + @pytest.mark.skip("Not consistently implemented") + def testWhoRealNameSpaces(self): + self._init() + + self.sendLine(2, "WHO :*UniqueReal Name") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self._checkReply(reply, "H") + + # What to do here? This? + # self.assertMessageMatch( + # end, + # command=RPL_ENDOFWHO, + # params=[ + # "otherNick", + # InsensitiveStr("*UniqueReal"), + # InsensitiveStr("Name"), + # ANYSTR, + # ], + # ) + + @pytest.mark.parametrize( + "mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"] + ) + @cases.mark_specifications("Modern") + def testWhoNickAway(self, mask): + self._init() + + self.sendLine(1, "AWAY :be right back") + self.getMessages(1) + self.getMessages(2) + + self.sendLine(2, f"WHO {mask}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self._checkReply(reply, "G") + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr(mask), ANYSTR], + ) + + @pytest.mark.parametrize( + "mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"] + ) + @cases.mark_specifications("Modern") + def testWhoNickOper(self, mask): + self._init() + + self.sendLine(1, "OPER operuser operpassword") + self.assertIn( + RPL_YOUREOPER, + [m.command for m in self.getMessages(1)], + fail_msg="OPER failed", + ) + + self.getMessages(2) + + self.sendLine(2, f"WHO {mask}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self._checkReply(reply, "H*") + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr(mask), ANYSTR], + ) + + @pytest.mark.parametrize( + "mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"] + ) + @cases.mark_specifications("Modern") + def testWhoNickAwayAndOper(self, mask): + self._init() + + self.sendLine(1, "OPER operuser operpassword") + self.assertIn( + RPL_YOUREOPER, + [m.command for m in self.getMessages(1)], + fail_msg="OPER failed", + ) + + self.sendLine(1, "AWAY :be right back") + self.getMessages(1) + self.getMessages(2) + + self.sendLine(2, f"WHO {mask}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self._checkReply(reply, "G*") + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr(mask), ANYSTR], + ) + + @pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"]) + @cases.mark_specifications("Modern") + def testWhoChan(self, mask): + self._init() + + self.sendLine(1, "OPER operuser operpassword") + self.assertIn( + RPL_YOUREOPER, + [m.command for m in self.getMessages(1)], + fail_msg="OPER failed", + ) + + self.sendLine(1, "AWAY :be right back") + self.getMessages(1) + self.getMessages(2) + + self.sendLine(2, f"WHO {mask}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 3, "Unexpected number of messages") + + (*replies, end) = messages + + # Get them in deterministic order + replies.sort(key=lambda msg: msg.params[5]) + + host_re = "[0-9A-Za-z_:.-]+" + self.assertMessageMatch( + replies[0], + command=RPL_WHOREPLY, + params=[ + "otherNick", + "#chan", + StrRe("~?" + self.username), + StrRe(host_re), + "My.Little.Server", + "coolNick", + "G*@", + StrRe(realname_regexp(self.realname)), + ], + ) + + self.assertMessageMatch( + replies[1], + command=RPL_WHOREPLY, + params=[ + "otherNick", + "#chan", + ANYSTR, + ANYSTR, + "My.Little.Server", + "otherNick", + "H", + StrRe("[0-9]+ .*"), + ], + ) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr(mask), ANYSTR], + )