irctest/irctest/server_tests/whois.py

321 lines
12 KiB
Python

import pytest
from irctest import cases
from irctest.numerics import (
RPL_AWAY,
RPL_ENDOFWHOIS,
RPL_WHOISACCOUNT,
RPL_WHOISACTUALLY,
RPL_WHOISCHANNELS,
RPL_WHOISHOST,
RPL_WHOISIDLE,
RPL_WHOISMODES,
RPL_WHOISOPERATOR,
RPL_WHOISREGNICK,
RPL_WHOISSECURE,
RPL_WHOISSERVER,
RPL_WHOISSPECIAL,
RPL_WHOISUSER,
RPL_YOUREOPER,
)
from irctest.patma import ANYSTR, StrRe
class _WhoisTestMixin(cases.BaseServerTestCase):
def _testWhoisNumerics(self, authenticate, away, oper):
if authenticate:
self.connectClient("nick1")
self.controller.registerUser(self, "val", "sesame")
self.connectClient(
"nick2", account="val", password="sesame", capabilities=["sasl"]
)
else:
self.connectClient("nick1")
self.connectClient("nick2")
self.sendLine(2, "JOIN #chan1")
self.sendLine(2, "JOIN #chan2")
if away:
self.sendLine(2, "AWAY :I'm on a break")
self.getMessages(2)
self.getMessages(1)
if oper:
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, "WHOIS nick2")
messages = []
for _ in range(10):
messages.extend(self.getMessages(1))
if RPL_ENDOFWHOIS in (m.command for m in messages):
break
last_message = messages.pop()
self.assertMessageMatch(
last_message,
command=RPL_ENDOFWHOIS,
params=["nick1", "nick2", ANYSTR],
fail_msg=f"Last message was not RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS})",
)
unexpected_messages = []
# Straight from the Modern spec
for m in messages:
if m.command == RPL_AWAY and away:
self.assertMessageMatch(m, params=["nick1", "nick2", "I'm on a break"])
elif m.command == RPL_WHOISREGNICK and authenticate:
self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR])
elif m.command == RPL_WHOISUSER:
self.assertMessageMatch(
m, params=["nick1", "nick2", ANYSTR, ANYSTR, "*", ANYSTR]
)
elif m.command == RPL_WHOISCHANNELS:
self.assertMessageMatch(
m,
params=[
"nick1",
"nick2",
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1)"),
],
)
elif m.command == RPL_WHOISSPECIAL:
# Technically allowed, but it's a bad style to use this without
# explicit configuration by the operators.
assert False, "RPL_WHOISSPECIAL in use with default configuration"
elif m.command == RPL_WHOISSERVER:
self.assertMessageMatch(
m, params=["nick1", "nick2", "My.Little.Server", ANYSTR]
)
elif m.command == RPL_WHOISOPERATOR:
# TODO: unlikely to ever send this, we should oper up nick2 first
self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR])
elif m.command == RPL_WHOISIDLE:
self.assertMessageMatch(
m,
params=["nick1", "nick2", StrRe("[0-9]+"), StrRe("[0-9]+"), ANYSTR],
)
elif m.command == RPL_WHOISACCOUNT and authenticate:
self.assertMessageMatch(m, params=["nick1", "nick2", "val", ANYSTR])
elif m.command == RPL_WHOISACTUALLY:
host_re = "[0-9a-z_:.-]+"
if len(m.params) == 4:
# Most common
self.assertMessageMatch(
m,
params=[
"nick1",
"nick2",
StrRe(host_re),
ANYSTR,
],
)
elif len(m.params) == 5:
# eg. Hybrid, Unreal
self.assertMessageMatch(
m,
params=[
"nick1",
"nick2",
StrRe(r"(~?username|\*)@" + host_re),
StrRe(host_re),
ANYSTR,
],
)
elif len(m.params) == 3:
# eg. Plexus4
self.assertMessageMatch(
m,
params=[
"nick1",
"nick2",
ANYSTR,
],
)
else:
assert (
False
), f"Unexpected number of params for RPL_WHOISACTUALLY: {m.params}"
elif m.command == RPL_WHOISHOST:
self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR])
elif m.command == RPL_WHOISMODES:
self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR])
elif m.command == RPL_WHOISSECURE:
# TODO: unlikely to ever send this, we should oper up nick2 first
self.assertMessageMatch(m, params=["nick1", "nick2", ANYSTR])
else:
unexpected_messages.append(m)
self.assertEqual(
unexpected_messages, [], fail_msg="Unexpected numeric messages: {got}"
)
class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper):
@pytest.mark.parametrize(
"server",
["", "My.Little.Server", "myCoolNick"],
ids=["no-target", "target_server", "target-nick"],
)
@cases.mark_specifications("RFC2812")
def testWhoisUser(self, server):
"""Test basic WHOIS behavior"""
nick = "myCoolNick"
username = "myusernam" # may be truncated if longer than this
realname = "My User Name"
self.addClient()
self.sendLine(1, f"NICK {nick}")
self.sendLine(1, f"USER {username} 0 * :{realname}")
self.skipToWelcome(1)
self.connectClient("otherNickname")
self.getMessages(2)
self.sendLine(2, f"WHOIS {server} mycoolnick")
messages = self.getMessages(2)
whois_user = messages[0]
self.assertEqual(whois_user.command, RPL_WHOISUSER)
# "<client> <nick> <username> <host> * :<realname>"
self.assertEqual(whois_user.params[1], nick)
self.assertIn(whois_user.params[2], ("~" + username, username))
# dumb regression test for oragono/oragono#355:
self.assertNotIn(
whois_user.params[3], [nick, username, "~" + username, realname]
)
self.assertEqual(whois_user.params[5], realname)
@pytest.mark.parametrize(
"away,oper",
[(False, False), (True, False), (False, True)],
ids=["normal", "away", "oper"],
)
@cases.mark_specifications("Modern")
def testWhoisNumerics(self, away, oper):
"""Tests all numerics are in the exhaustive list defined in the Modern spec.
TBD modern PR"""
self._testWhoisNumerics(authenticate=False, away=away, oper=oper)
@cases.mark_services
class ServicesWhoisTestCase(
_WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper
):
@pytest.mark.parametrize("oper", [False, True], ids=["normal", "oper"])
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
@cases.mark_specifications("Modern")
def testWhoisNumerics(self, oper):
"""Tests all numerics are in the exhaustive list defined in the Modern spec,
on an authenticated user.
TBD modern PR"""
self._testWhoisNumerics(oper=oper, authenticate=True, away=False)
@cases.mark_specifications("Ergo")
def testInvisibleWhois(self):
"""Test interaction between MODE +i and RPL_WHOISCHANNELS."""
self.connectClient("userOne")
self.joinChannel(1, "#xyz")
self.connectClient("userTwo")
self.getMessages(2)
self.sendLine(2, "WHOIS userOne")
commands = {m.command for m in self.getMessages(2)}
self.assertIn(
RPL_WHOISCHANNELS,
commands,
"RPL_WHOISCHANNELS should be sent for a non-invisible nick",
)
self.getMessages(1)
self.sendLine(1, "MODE userOne +i")
message = self.getMessage(1)
self.assertEqual(
message.command,
"MODE",
"Expected MODE reply, but received {}".format(message.command),
)
self.assertEqual(
message.params,
["userOne", "+i"],
"Expected user set +i, but received {}".format(message.params),
)
self.getMessages(2)
self.sendLine(2, "WHOIS userOne")
commands = {m.command for m in self.getMessages(2)}
self.assertNotIn(
RPL_WHOISCHANNELS,
commands,
"RPL_WHOISCHANNELS should not be sent for an invisible nick"
"unless the user is also a member of the channel",
)
self.sendLine(2, "JOIN #xyz")
self.sendLine(2, "WHOIS userOne")
commands = {m.command for m in self.getMessages(2)}
self.assertIn(
RPL_WHOISCHANNELS,
commands,
"RPL_WHOISCHANNELS should be sent for an invisible nick"
"if the user is also a member of the channel",
)
self.sendLine(2, "PART #xyz")
self.getMessages(2)
self.getMessages(1)
self.sendLine(1, "MODE userOne -i")
message = self.getMessage(1)
self.assertEqual(
message.command,
"MODE",
"Expected MODE reply, but received {}".format(message.command),
)
self.assertEqual(
message.params,
["userOne", "-i"],
"Expected user set -i, but received {}".format(message.params),
)
self.sendLine(2, "WHOIS userOne")
commands = {m.command for m in self.getMessages(2)}
self.assertIn(
RPL_WHOISCHANNELS,
commands,
"RPL_WHOISCHANNELS should be sent for a non-invisible nick",
)
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
@cases.mark_specifications("ircdocs")
def testWhoisAccount(self):
"""Test numeric 330, RPL_WHOISACCOUNT.
<https://defs.ircdocs.horse/defs/numerics.html#rpl-whoisaccount-330>"""
self.controller.registerUser(self, "shivaram", "sesame")
self.connectClient(
"netcat", account="shivaram", password="sesame", capabilities=["sasl"]
)
self.getMessages(1)
self.connectClient("curious")
self.sendLine(2, "WHOIS netcat")
messages = self.getMessages(2)
# 330 RPL_WHOISACCOUNT
whoisaccount = [message for message in messages if message.command == "330"]
self.assertEqual(len(whoisaccount), 1, messages)
params = whoisaccount[0].params
# <client> <nick> <authname> :<info>
self.assertEqual(len(params), 4)
self.assertEqual(params[:3], ["curious", "netcat", "shivaram"])
self.sendLine(1, "WHOIS curious")
messages = self.getMessages(2)
whoisaccount = [message for message in messages if message.command == "330"]
self.assertEqual(len(whoisaccount), 0)