Compare commits

...

11 Commits

Author SHA1 Message Date
Shivaram Lingamneni df3ba285f8
Merge 926592f5ef into 9f8e712776 2024-04-20 09:39:37 +02:00
Val Lorentz 9f8e712776 testNonutf8Realname/testNonutf8Username: Add support for ERROR instead of FAIL/ERR_INVALIDUSERNAME
This is what Sable does, at it fails to decode non-UTF8 data before
it even tries to parse commands.
2024-04-19 15:43:21 +02:00
Val Lorentz a1f8fcac49 testNonutf8Username: Actually test a non-UTF8 username 2024-04-19 15:43:21 +02:00
Valentin Lorentz d3c919e0f5 dashboard: Fix for parametrized tests 2024-04-19 15:16:36 +02:00
Val Lorentz ce51dddc15
Display method docstrings on the dashboard (#270)
Collapsed with <details> because they can be pretty long and make the table
harder to read.
2024-04-19 15:15:27 +02:00
Val Lorentz 7f9b4b315f
xfail testJoinNamreply on Bahamut and irc2 (#269) 2024-04-17 20:26:18 +02:00
Val Lorentz 9d43a002c2
Simplify multi-prefix-related tests and add testNoMultiPrefix (#262)
* Simplify RPL_NAMREPLY-on-join tests

* Simplify testMultiPrefix

* Add testNoMultiPrefix
2024-04-16 21:25:35 +02:00
Val Lorentz ea66a8f9a4
Make re.match actually check the whole string matches the pattern (#261)
And explicitly allow trailing space in RPL_WHOISCHANNELS
2024-04-16 21:05:25 +02:00
Valentin Lorentz 473db1cc5b ngircd: Disable PAM
It breaks irctest when ngircd was compiled with --with-pam
2024-04-16 21:00:24 +02:00
Shivaram Lingamneni 926592f5ef allow a trailing space on the nick list 2024-04-14 00:19:28 -04:00
Shivaram Lingamneni ce3ad40298 add test for RPL_NAMREPLY for secret channels 2024-04-13 22:10:34 -04:00
11 changed files with 156 additions and 110 deletions

View File

@ -23,6 +23,7 @@ TEMPLATE_CONFIG = """
[Options]
MorePrivacy = no # by default, always replies to WHOWAS with ERR_WASNOSUCHNICK
PAM = no
[Operator]
Name = operuser

View File

@ -246,7 +246,17 @@ def build_test_table(
row_anchor = md5sum(row_anchor)
row = HTML.tr(
HTML.th(HTML.a(test_name, href=f"#{row_anchor}"), class_="test-name"),
HTML.th(
HTML.details(
HTML.summary(HTML.a(test_name, href=f"#{row_anchor}")),
docstring(
getattr(
getattr(module, class_name), test_name.split("[")[0]
)
),
),
class_="test-name",
),
id=row_anchor,
)
rows.append(row)

View File

@ -1,23 +0,0 @@
"""
Handles ambiguities of RFCs.
"""
from typing import List
def normalize_namreply_params(params: List[str]) -> List[str]:
# So… RFC 2812 says:
# "( "=" / "*" / "@" ) <channel>
# :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
# but spaces seem to be missing (eg. before the colon), so we
# don't know if there should be one before the <channel> and its
# prefix.
# So let's normalize this to “with space”, and strip spaces at the
# end of the nick list.
params = list(params) # copy the list
if len(params) == 3:
assert params[1][0] in "=*@", params
params.insert(1, params[1][0])
params[2] = params[2][1:]
params[3] = params[3].rstrip()
return params

View File

@ -15,7 +15,7 @@ TAG_ESCAPE = [
unescape_tag_value = MultipleReplacer(dict(map(lambda x: (x[1], x[0]), TAG_ESCAPE)))
# TODO: validate host
tag_key_validator = re.compile(r"\+?(\S+/)?[a-zA-Z0-9-]+")
tag_key_validator = re.compile(r"^\+?(\S+/)?[a-zA-Z0-9-]+$")
def parse_tags(s: str) -> Dict[str, Optional[str]]:

View File

@ -106,15 +106,15 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo
elif isinstance(expected, _AnyStr) and got is not None:
return True
elif isinstance(expected, StrRe):
if got is None or not re.match(expected.regexp, got):
if got is None or not re.match(expected.regexp + "$", got):
return False
elif isinstance(expected, OptStrRe):
if got is None:
return True
if not re.match(expected.regexp, got):
if not re.match(expected.regexp + "$", got):
return False
elif isinstance(expected, NotStrRe):
if got is None or re.match(expected.regexp, got):
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():

View File

@ -56,7 +56,8 @@ class IsupportTestCase(cases.BaseServerTestCase):
return
m = re.match(
r"\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)", self.server_support["PREFIX"]
r"^\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)$",
self.server_support["PREFIX"],
)
self.assertTrue(
m,
@ -117,5 +118,5 @@ class IsupportTestCase(cases.BaseServerTestCase):
parts = self.server_support["TARGMAX"].split(",")
for part in parts:
self.assertTrue(
re.match("[A-Z]+:[0-9]*", part), "Invalid TARGMAX key:value: %r", part
re.match("^[A-Z]+:[0-9]*$", part), "Invalid TARGMAX key:value: %r", part
)

View File

@ -6,7 +6,6 @@ The JOIN command (`RFC 1459
"""
from irctest import cases, runner
from irctest.irc_utils import ambiguities
from irctest.numerics import (
ERR_BADCHANMASK,
ERR_FORBIDDENCHANNEL,
@ -61,6 +60,7 @@ class JoinTestCase(cases.BaseServerTestCase):
),
)
@cases.xfailIfSoftware(["Bahamut", "irc2"], "trailing space on RPL_NAMREPLY")
@cases.mark_specifications("RFC2812")
def testJoinNamreply(self):
"""“353 RPL_NAMREPLY
@ -75,33 +75,23 @@ class JoinTestCase(cases.BaseServerTestCase):
for m in self.getMessages(1):
if m.command == "353":
self.assertIn(
len(m.params),
(3, 4),
m,
fail_msg="RPL_NAM_REPLY with number of arguments "
"<3 or >4: {msg}",
self.assertMessageMatch(
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
)
params = ambiguities.normalize_namreply_params(m.params)
self.assertIn(
params[1],
"=*@",
self.connectClient("bar")
self.sendLine(2, "JOIN #chan")
for m in self.getMessages(2):
if m.command == "353":
self.assertMessageMatch(
m,
fail_msg="Bad channel prefix: {item} not in {list}: {msg}",
)
self.assertEqual(
params[2],
"#chan",
m,
fail_msg="Bad channel name: {got} instead of " "{expects}: {msg}",
)
self.assertIn(
params[3],
{"foo", "@foo", "+foo"},
m,
fail_msg="Bad user list: should contain only user "
'"foo" with an optional "+" or "@" prefix, but got: '
"{msg}",
params=[
"bar",
StrRe(r"[=\*@]"),
"#chan",
StrRe("([@+]?foo bar|bar [@+]?foo)"),
],
)
def testJoinTwice(self):
@ -115,34 +105,8 @@ class JoinTestCase(cases.BaseServerTestCase):
# if the join is successful, or has an error among the given set.
for m in self.getMessages(1):
if m.command == "353":
self.assertIn(
len(m.params),
(3, 4),
m,
fail_msg="RPL_NAM_REPLY with number of arguments "
"<3 or >4: {msg}",
)
params = ambiguities.normalize_namreply_params(m.params)
self.assertIn(
params[1],
"=*@",
m,
fail_msg="Bad channel prefix: {item} not in {list}: {msg}",
)
self.assertEqual(
params[2],
"#chan",
m,
fail_msg="Bad channel name: {got} instead of " "{expects}: {msg}",
)
self.assertIn(
params[3],
{"foo", "@foo", "+foo"},
m,
fail_msg='Bad user list after user "foo" joined twice '
"the same channel: should contain only user "
'"foo" with an optional "+" or "@" prefix, but got: '
"{msg}",
self.assertMessageMatch(
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
)
def testJoinPartiallyInvalid(self):

View File

@ -24,11 +24,6 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
self.sendLine(1, "NAMES #chan")
reply = self.getMessage(1)
self.assertMessageMatch(
reply,
command="353",
fail_msg="Expected NAMES response (353) with @+foo, got: {msg}",
)
self.assertMessageMatch(
reply,
command="353",
@ -47,9 +42,57 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
8,
"Expected WHO response (352) with 8 params, got: {msg}".format(msg=msg),
)
self.assertTrue(
"@+" in msg.params[6],
self.assertIn(
"@+",
msg.params[6],
'Expected WHO response (352) with "@+" in param 7, got: {msg}'.format(
msg=msg
),
)
@cases.xfailIfSoftware(
["irc2", "Bahamut"], "irc2 and Bahamut send a trailing space"
)
def testNoMultiPrefix(self):
"""When not requested, only the highest prefix should be sent"""
self.connectClient("foo")
self.joinChannel(1, "#chan")
self.sendLine(1, "MODE #chan +v foo")
self.getMessages(1)
# TODO(dan): Make sure +v is voice
self.sendLine(1, "NAMES #chan")
reply = self.getMessage(1)
self.assertMessageMatch(
reply,
command="353",
params=["foo", ANYSTR, "#chan", "@foo"],
fail_msg="Expected NAMES response (353) with @foo, got: {msg}",
)
self.getMessages(1)
self.sendLine(1, "WHO #chan")
msg = self.getMessage(1)
self.assertEqual(
msg.command, "352", msg, fail_msg="Expected WHO response (352), got: {msg}"
)
self.assertGreaterEqual(
len(msg.params),
8,
"Expected WHO response (352) with 8 params, got: {msg}".format(msg=msg),
)
self.assertIn(
"@",
msg.params[6],
'Expected WHO response (352) with "@" in param 7, got: {msg}'.format(
msg=msg
),
)
self.assertNotIn(
"+",
msg.params[6],
'Expected WHO response (352) with no "+" in param 7, got: {msg}'.format(
msg=msg
),
)

View File

@ -11,7 +11,7 @@ from irctest.patma import ANYSTR, StrRe
class NamesTestCase(cases.BaseServerTestCase):
def _testNames(self, symbol):
def _testNames(self, symbol: bool, allow_trailing_space: bool):
self.connectClient("nick1")
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
@ -31,7 +31,10 @@ class NamesTestCase(cases.BaseServerTestCase):
"nick1",
*(["="] if symbol else []),
"#chan",
StrRe("(nick2 @nick1|@nick1 nick2)"),
StrRe(
"(nick2 @nick1|@nick1 nick2)"
+ (" ?" if allow_trailing_space else "")
),
],
)
@ -44,20 +47,50 @@ class NamesTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", deprecated=True)
def testNames1459(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNames(symbol=False)
self._testNames(symbol=False, allow_trailing_space=True)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.mark_specifications("RFC2812", "Modern")
def testNames2812(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNames(symbol=True)
self._testNames(symbol=True, allow_trailing_space=True)
@cases.mark_specifications("Modern")
@cases.xfailIfSoftware(
["Bahamut", "irc2"], "Bahamut and irc2 send a trailing space in RPL_NAMREPLY"
)
def testNamesModern(self):
"""
https://modern.ircdocs.horse/#names-message
"""
self._testNames(symbol=True, allow_trailing_space=False)
@cases.mark_specifications("RFC2812", "Modern")
def testNames2812Secret(self):
"""The symbol sent for a secret channel is `@` instead of `=`:
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
https://modern.ircdocs.horse/#rplnamreply-353
"""
self.connectClient("nick1")
self.sendLine(1, "JOIN #chan")
# enable secret channel mode
self.sendLine(1, "MODE #chan +s")
self.getMessages(1)
self.sendLine(1, "NAMES #chan")
messages = self.getMessages(1)
self.assertMessageMatch(
messages[0],
command=RPL_NAMREPLY,
params=["nick1", "@", "#chan", StrRe("@nick1 ?")],
)
self.assertMessageMatch(
messages[1],
command=RPL_ENDOFNAMES,
params=["nick1", "#chan", ANYSTR],
)
def _testNamesMultipleChannels(self, symbol):
self.connectClient("nick1")

View File

@ -57,8 +57,16 @@ class Utf8TestCase(cases.BaseServerTestCase):
self.sendLine(2, "NICK bar")
self.clients[2].conn.sendall(b"USER username * * :i\xe8rc\xe9\r\n")
d = self.clients[2].conn.recv(1024)
if b"FAIL " in d or b"468 " in d: # ERR_INVALIDUSERNAME
d = b""
while True:
try:
buf = self.clients[2].conn.recv(1024)
except TimeoutError:
break
if d and not buf:
break
d += buf
if b"FAIL " in d or b"ERROR " in d or b"468 " in d: # ERR_INVALIDUSERNAME
return # nothing more to test
self.assertIn(b"001 ", d)
@ -72,14 +80,21 @@ class Utf8TestCase(cases.BaseServerTestCase):
self.addClient()
self.sendLine(2, "NICK bar")
self.sendLine(2, "USER 😊😊😊😊😊😊😊😊😊😊 * * :realname")
m = self.getRegistrationMessage(2)
if m.command in ("FAIL", "468"): # ERR_INVALIDUSERNAME
self.clients[2].conn.sendall(b"USER \xe8rc\xe9 * * :readlname\r\n")
d = b""
while True:
try:
buf = self.clients[2].conn.recv(1024)
except TimeoutError:
break
if d and not buf:
break
d += buf
if b"FAIL " in d or b"ERROR " in d or b"468 " in d: # ERR_INVALIDUSERNAME
return # nothing more to test
self.assertMessageMatch(
m,
command="001",
)
self.assertIn(b"001 ", d)
self.sendLine(2, "WHOIS bar")
self.getMessages(2)

View File

@ -97,7 +97,9 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
params=[
"nick1",
"nick2",
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1)"),
# trailing space was required by the RFCs, and Modern explicitly
# allows it
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1) ?"),
],
)
elif m.command == RPL_WHOISSPECIAL: