Compare commits

...

15 Commits

Author SHA1 Message Date
Val Lorentz d9690bd2ad
Merge 754df829d4 into 61ae4bcf9e 2024-04-13 10:08:49 +02:00
Sadie Powell 61ae4bcf9e
Relink the modules directory as well as the lib directory. (#260) 2024-04-04 17:47:48 +02:00
Sadie Powell 0c5c91368a
Pass --nopid to Anope. (#259) 2024-03-21 21:04:13 +01:00
Shivaram Lingamneni c0e6ca4dde
add a test for WHOIS on nonexistent users (#258)
* add a test for WHOIS on nonexistent users

* skip test in Sable for now
2024-03-19 10:30:44 -04:00
Valentin Lorentz 754df829d4 Add tests for SUB 2023-04-16 09:19:50 +02:00
Valentin Lorentz 10c1772121 Use real metadata keys 2023-04-16 09:19:50 +02:00
Valentin Lorentz 6c5595539c Add tests for KEY_NO_PERMISSION 2023-04-16 09:19:50 +02:00
Valentin Lorentz 34f078e04a Add a bunch of GetSet tests 2023-04-16 09:19:50 +02:00
Valentin Lorentz f18162c02d Add untested 'before-connect' test 2023-04-16 09:19:50 +02:00
Valentin Lorentz 8a6a12e7f7 Fix fail code 2023-04-16 09:19:50 +02:00
Valentin Lorentz 0318a37075 Test for nick instead of * as target 2023-04-16 09:19:50 +02:00
Valentin Lorentz 8d01063aee Improve testing for invalid UTF8, and add test for too-long values 2023-04-16 09:19:50 +02:00
Valentin Lorentz 88f8be0ff6 expect batches 2023-04-16 09:19:50 +02:00
Valentin Lorentz e17ab347f4 Adapt metadata_2 to stdreplies, and run it on Unreal
Passes on 3968354247
2023-04-16 09:19:50 +02:00
Valentin Lorentz 604041aa13 Copy metadata.py to metadata2.py 2023-04-16 09:19:50 +02:00
6 changed files with 658 additions and 5 deletions

View File

@ -74,7 +74,19 @@ class ClientMock:
continue
if not synchronize:
got_pong = True
for line in data.decode().split("\r\n"):
try:
decoded_data = data.decode()
except UnicodeDecodeError:
print(
"{time:.3f}{ssl} S -> {client} - failed to decode: {data!r}".format(
time=time.time(),
ssl=" (ssl)" if self.ssl else "",
client=self.name,
data=data,
)
)
raise
for line in decoded_data.split("\r\n"):
if line:
if self.show_io:
print(

View File

@ -132,14 +132,16 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
# Config and code need to be in the same directory, *obviously*
(self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib")
(self.directory / "modules").symlink_to(
Path(services_path).parent.parent / "modules"
)
self.proc = subprocess.Popen(
[
"anope",
"-n", # don't fork
"--config=services.conf", # can't be an absolute path
# "--logdir",
# f"/tmp/services-{server_port}.log",
"--config=services.conf", # can't be an absolute path in 2.0
"--nofork", # don't fork
"--nopid", # don't write a pid
],
cwd=self.directory,
# stdout=subprocess.DEVNULL,

View File

@ -11,6 +11,7 @@ from irctest.basecontrollers import BaseServerController, DirectoryBasedControll
TEMPLATE_CONFIG = """
include "modules.default.conf";
loadmodule "third/metadata2";
include "operclass.default.conf";
{extras}
include "help/help.conf";

View File

@ -0,0 +1,617 @@
"""
`IRCv3 Metadata 2 <https://github.com/ircv3/ircv3-specifications/pull/501>`_
(not to be confused with the `deprecated IRCv3 Metadata
<https://ircv3.net/specs/core/metadata-3.2>`_)
"""
import itertools
import pytest
from irctest import cases, runner
from irctest.patma import ANYDICT, ANYLIST, ANYSTR, StrRe
CLIENT_NICKS = {
1: "foo",
2: "bar",
}
class MetadataTestCase(cases.BaseServerTestCase):
def getBatchMessages(self, client):
messages = self.getMessages(client)
first_msg = messages.pop(0)
last_msg = messages.pop(-1)
self.assertMessageMatch(
first_msg, command="BATCH", params=[StrRe(r"\+.*"), "metadata"]
)
batch_id = first_msg.params[0][1:]
self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id])
return (batch_id, messages)
def sub(self, client, keys):
self.sendLine(2, "METADATA * SUB " + " ".join(keys))
acknowledged_subs = []
for m in self.getMessages(2):
self.assertMessageMatch(
m,
command="770", # RPL_METADATASUBOK
params=["bar", *ANYLIST],
)
acknowledged_subs.extend(m.params[1:])
self.assertEqual(
sorted(acknowledged_subs),
sorted(keys),
fail_msg="Expected RPL_METADATASUBOK to ack {expects}, got {got}",
)
@cases.mark_specifications("IRCv3")
def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 1, fail_msg="Expected one ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
tags={"batch": batch_id, **ANYDICT},
command="766", # ERR_NOMATCHINGKEY
fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a "
"request to an unset valid METADATA key: {msg}",
)
@cases.mark_specifications("IRCv3")
def testGetTwoUnsetValid(self):
"""“Multiple keys may be given. The response will be either RPL_KEYVALUE,
ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name avatar")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 2, fail_msg="Expected two ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[0],
params=["foo", "foo", "display-name", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to display-name first: {msg}",
)
self.assertMessageMatch(
messages[1],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with two 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[1],
params=["foo", "foo", "avatar", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to avatar as second response: {msg}",
)
@cases.mark_specifications("IRCv3")
def testListNoSet(self):
"""“This subcommand MUST list all currently-set metadata keys along
with their values. The response will be zero or more RPL_KEYVALUE
events, following by RPL_METADATAEND event.
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * LIST")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 0, fail_msg="Expected empty batch")
@cases.mark_specifications("IRCv3")
def testListInvalidTarget(self):
"""“In case of invalid target RPL_METADATAEND MUST NOT be sent.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA foobar LIST")
m = self.getMessage(1)
self.assertMessageMatch(
m,
command="FAIL",
params=["METADATA", "INVALID_TARGET", "foobar", ANYSTR],
fail_msg="Response to “METADATA <invalid target> LIST” was "
"not 765 (ERR_TARGETINVALID) but: {msg}",
)
commands = {m.command for m in self.getMessages(1)}
self.assertNotIn(
"762",
commands,
fail_msg="Sent “METADATA <invalid target> LIST”, got FAIL INVALID_TARGET, "
"and then 762 (RPL_METADATAEND)",
)
def assertSetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} SET {} :{}".format(target, key, value))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertUnsetValue(self, client, target, key):
self.sendLine(client, "METADATA {} SET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertGetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertValueNotSet(self, client, target, key):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertSetGetValue(self, client, target, key, value):
self.assertSetValue(client, target, key, value)
self.assertGetValue(client, target, key, value)
def assertUnsetGetValue(self, client, target, key):
self.assertUnsetValue(client, target, key)
self.assertValueNotSet(client, target, key)
@pytest.mark.parametrize(
"set_target,get_target", itertools.product(["*", "foo"], ["*", "foo"])
)
@cases.mark_specifications("IRCv3")
def testSetGetUser(self, set_target, get_target):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, set_target, "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetUserAgain(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
@cases.mark_specifications("IRCv3")
def testSetUnsetUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertUnsetGetValue(1, "*", "display-name")
@cases.mark_specifications("IRCv3")
def testGetOtherUser(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "foo", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetOtherUser(self):
"""Not required by the spec, but it makes little sense to allow anyone to
write a channel's metadata"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other.
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(1, "METADATA bar SET display-name :Totally Not Foo")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "bar", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The Second"],
)
self.assertUnsetGetValue(1, "*", "display-name")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR],
)
@cases.mark_specifications("IRCv3")
def testSubUserSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set its display-name even though "
"they don't share a channel",
)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
@cases.mark_specifications("IRCv3")
def testSetGetChannel(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "#chan", "display-name", "Hash Channel")
@cases.mark_specifications("IRCv3")
def testSetUnsetChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertUnsetGetValue(1, "#chan", "display-name")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertValueNotSet(2, "#chan", "display-name")
@cases.mark_specifications("IRCv3")
def testSetGetChannelNotOp(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(2, "METADATA #chan SET display-name :Sharp Channel")
self.assertMessageMatch(
self.getMessage(2),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "#chan", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(1),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
self.assertSetGetValue(1, "#chan", "display-name", "Harsh Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Harsh Channel"],
)
@cases.mark_specifications("IRCv3")
def testSubChannelSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set #chan's display-name even "
"though they are not in it",
)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
@cases.mark_specifications("IRCv3")
def testSetGetValidBeforeConnect(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.addClient(1)
self.sendLine(1, "CAP LS 302")
caps = self.getCapLs(1)
if "before-connect" not in (caps["draft/metadata-2"] or "").split(","):
raise runner.OptionalExtensionNotSupported(
"draft/metadata-2=before-connect"
)
self.requestCapabilities(1, ["draft/metadata-2", "batch"], skip_if_cap_nak=True)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.sendLine(1, "NICK foo")
self.sendLine(1, "USER foo 0 * :foo")
self.sendLine(1, "CAP END")
self.skipToWelcome(1)
self.assertGetValue(1, "*", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetHeartInValue(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
heart = b"\xf0\x9f\x92\x9c".decode()
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(
1,
"*",
"display-name",
"->{}<-".format(heart),
)
def _testSetInvalidValue(self, value):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# Sending directly because it is not valid UTF-8 so Python would
# not like it
self.clients[1].conn.sendall(
b"METADATA * SET display-name :invalid UTF-8 ->\xc3<-\r\n"
)
try:
commands = {m.command for m in self.getMessages(1)}
except UnicodeDecodeError:
assert False, "Server sent invalid UTF-8"
self.assertNotIn(
"761",
commands, # RPL_KEYVALUE
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.clients[1].conn.sendall(b"METADATA * SET display-name :" + value + b"\r\n")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "VALUE_INVALID", ANYSTR],
)
messages = self.getMessages(1)
self.assertNotIn(
"761", # RPL_KEYVALUE
{m.command for m in messages},
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.assertEqual(
messages,
[],
fail_msg="Unexpected response to METADATA SET with invalid value: {got}",
)
@cases.mark_specifications("IRCv3")
def testSetInvalidUtf8(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
self._testSetInvalidValue(b"invalid UTF-8: \xc3")
@cases.mark_specifications("IRCv3")
def testSetTooManyChars(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
self._testSetInvalidValue(b"abcd" * 120)
@cases.mark_specifications("IRCv3")
def testSetTooManyBytes(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
heart = b"\xf0\x9f\x92\x9c"
self._testSetInvalidValue(heart * 120)

View File

@ -8,6 +8,7 @@ import pytest
from irctest import cases
from irctest.numerics import (
ERR_NOSUCHNICK,
RPL_AWAY,
RPL_ENDOFWHOIS,
RPL_WHOISACCOUNT,
@ -217,6 +218,25 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
whois_user.params[3], [nick, username, "~" + username, realname]
)
@cases.mark_specifications("RFC2812")
@cases.xfailIfSoftware(["Sable"], "https://github.com/Libera-Chat/sable/issues/101")
def testWhoisMissingUser(self):
"""Test WHOIS on a nonexistent nickname."""
self.connectClient("qux", name="qux")
self.sendLine("qux", "WHOIS bar")
messages = self.getMessages("qux")
self.assertEqual(len(messages), 2)
self.assertMessageMatch(
messages[0],
command=ERR_NOSUCHNICK,
params=["qux", "bar", ANYSTR],
)
self.assertMessageMatch(
messages[1],
command=RPL_ENDOFWHOIS,
params=["qux", "bar", ANYSTR],
)
@pytest.mark.parametrize(
"away,oper",
[(False, False), (True, False), (False, True)],

View File

@ -27,6 +27,7 @@ markers =
extended-monitor
labeled-response
message-tags
metadata-2
draft/multiline
multi-prefix
server-time