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
This commit is contained in:
Val Lorentz 2021-12-23 17:15:10 +01:00 committed by GitHub
parent 05e78802ca
commit 6c393c4e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 341 additions and 0 deletions

View File

@ -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

View File

@ -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:

325
irctest/server_tests/who.py Normal file
View File

@ -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")
# " `<mask>` MUST be exactly the `<mask>` 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")
# " `<mask>` MUST be exactly the `<mask>` 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")
# " `<mask>` MUST be exactly the `<mask>` 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],
)