Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion irctest/server_tests/away.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`Modern <https://modern.ircdocs.horse/#away-message>`__)
"""

from irctest import cases
from irctest import cases, runner
from irctest.numerics import (
RPL_AWAY,
RPL_NOWAWAY,
Expand Down Expand Up @@ -181,3 +181,45 @@ def testAwayEmptyMessage(self):
replies = self.getMessages("qux")
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
self.assertNotIn(RPL_AWAY, [msg.command for msg in replies])

@cases.mark_specifications("Modern")
@cases.mark_isupport("AWAYLEN")
def testAwaylen(self):
"""
"AWAYLEN=<number>
The AWAYLEN parameter indicates the maximum length for the <reason> of an AWAY command."
-- https://modern.ircdocs.horse/#awaylen-parameter
"""
self.connectClient("foo")

if "AWAYLEN" not in self.server_support:
raise runner.IsupportTokenNotSupported("AWAYLEN")

awaylen = int(self.server_support["AWAYLEN"])

# Set away message at exactly the limit
valid_away = "a" * awaylen
self.sendLine(1, f"AWAY :{valid_away}")
self.assertMessageMatch(
self.getMessage(1), command="306", params=["foo", ANYSTR]
) # RPL_NOWAWAY

# Set away message longer than the limit
long_away = "b" * (awaylen + 50)
self.sendLine(1, f"AWAY :{long_away}")
self.getMessages(1)

# Check the away message
self.connectClient("bar")
self.sendLine(2, "WHOIS foo")
msgs = self.getMessages(2)

away_msgs = [m for m in msgs if m.command == "301"]
self.assertMessageMatch(
away_msgs[0], command="301", params=["bar", "foo", ANYSTR]
)
self.assertLessEqual(
len(away_msgs[0].params[2]),
awaylen,
f"Server sent away message longer than AWAYLEN {awaylen}",
)
137 changes: 137 additions & 0 deletions irctest/server_tests/isupport_limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
Tests for ISUPPORT limit enforcement (`Modern
<https://modern.ircdocs.horse/#rplisupport-parameters>`__)
"""

import pytest

from irctest import cases, runner
from irctest.numerics import (
ERR_BADCHANMASK,
ERR_ERRONEUSNICKNAME,
ERR_FORBIDDENCHANNEL,
ERR_NOSUCHCHANNEL,
ERR_TOOMANYCHANNELS,
)
from irctest.patma import ANYSTR, Either

ERR_BADCHANNAME = "479" # Hybrid only, and conflicts with others


class IsupportLimitTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
@cases.mark_isupport("CHANLIMIT")
@pytest.mark.parametrize("prefix", ["#", "&"])
def testChanlimit(self, prefix):
"""
"CHANLIMIT=<prefixes>:[limit]{,<prefixes>:[limit]}"
-- https://modern.ircdocs.horse/#chanlimit-parameter
"""
self.connectClient("foo")

if "CHANLIMIT" not in self.server_support:
raise runner.IsupportTokenNotSupported("CHANLIMIT")

pairs = [
part.split(":", 1) for part in self.server_support["CHANLIMIT"].split(",")
]
limits = {pair[0]: int(pair[1]) if pair[1] else None for pair in pairs}

self.assertTrue(limits, "CHANLIMIT is empty")

limit = limits.get(prefix)
if limit is None:
raise runner.ImplementationChoice(f"No limit for {prefix} channels")

# Join up to the limit
for i in range(limit):
self.sendLine(1, f"JOIN {prefix}chan{i}")
self.assertMessageMatch(
self.getMessage(1),
command="JOIN",
params=[f"{prefix}chan{i}"],
fail_msg=f"Failed to join channel {i + 1}/{limit}",
)
self.getMessages(1) # clear any remaining messages

# Try to join one more - should fail
self.sendLine(1, f"JOIN {prefix}chan")
self.assertMessageMatch(
self.getMessage(1),
command=ERR_TOOMANYCHANNELS,
params=["foo", f"{prefix}chan", ANYSTR],
)

@cases.mark_specifications("Modern")
@cases.mark_isupport("CHANNELLEN")
@pytest.mark.parametrize("prefix", ["#", "&"])
def testChannellen(self, prefix):
"""
"CHANNELLEN=<number>
The CHANNELLEN parameter specifies the maximum length of a channel name that a client may join."
-- https://modern.ircdocs.horse/#channellen-parameter
"""
self.connectClient("foo")

if "CHANNELLEN" not in self.server_support:
raise runner.IsupportTokenNotSupported("CHANNELLEN")

chantypes = self.server_support.get("CHANTYPES", "#")
if prefix not in chantypes:
raise runner.NotImplementedByController(
f"Server does not support {prefix} channels"
)

channellen = int(self.server_support["CHANNELLEN"])

# Try a channel name at exactly the limit
valid_chan = prefix + "a" * (channellen - 1)
self.sendLine(1, f"JOIN {valid_chan}")
self.assertMessageMatch(self.getMessage(1), command="JOIN", params=[valid_chan])

# Try a channel name longer than the limit
self.getMessages(1) # clear
invalid_chan = prefix + "b" * channellen
self.sendLine(1, f"JOIN {invalid_chan}")
self.assertMessageMatch(
self.getMessage(1),
command=Either(
ERR_NOSUCHCHANNEL,
ERR_BADCHANNAME,
ERR_FORBIDDENCHANNEL,
ERR_BADCHANMASK,
),
params=["foo", invalid_chan, ANYSTR],
)

@cases.mark_specifications("Modern")
@cases.mark_isupport("NICKLEN")
def testNicklen(self):
"""
"NICKLEN=<number>
"The NICKLEN parameter indicates the maximum length of a nickname that a client may set.
Clients on the network MAY have longer nicks than this.
The value MUST be specified and MUST be a positive integer.
30 or 31 are typical values for this parameter advertised by servers today."
-- https://modern.ircdocs.horse/#nicklen-parameter
"""
self.connectClient("foo")

if "NICKLEN" not in self.server_support:
raise runner.IsupportTokenNotSupported("NICKLEN")

nicklen = int(self.server_support["NICKLEN"])

# Try a nick at exactly the limit
valid_nick = "a" * nicklen
self.sendLine(1, f"NICK {valid_nick}")
self.assertMessageMatch(self.getMessage(1), command="NICK", params=[valid_nick])

# Try a nick longer than the limit
invalid_nick = "b" * (nicklen + 5)
self.sendLine(1, f"NICK {invalid_nick}")
self.assertMessageMatch(
self.getMessage(1),
command=ERR_ERRONEUSNICKNAME,
params=[valid_nick, invalid_nick, ANYSTR],
)
48 changes: 48 additions & 0 deletions irctest/server_tests/kick.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,51 @@ def testDoubleKickMessages(self, multiple_targets):
m1.params[1], m2.params[1]
)
)

@cases.mark_specifications("Modern")
@cases.mark_isupport("KICKLEN")
def testKicklen(self):
"""
"KICKLEN=<number>
The KICKLEN parameter indicates the maximum length for the <reason> of a KICK command."
-- https://modern.ircdocs.horse/#kicklen-parameter
"""
self.connectClient("foo")
self.joinChannel(1, "#test")

if "KICKLEN" not in self.server_support:
raise runner.IsupportTokenNotSupported("KICKLEN")

self.connectClient("bar")
self.joinChannel(2, "#test")
self.getMessages(1)

kicklen = int(self.server_support["KICKLEN"])

# Kick with a reason at exactly the limit
valid_reason = "a" * kicklen
self.sendLine(1, f"KICK #test bar :{valid_reason}")
msg = self.getMessage(1)
self.assertMessageMatch(msg, command="KICK", params=["#test", "bar", ANYSTR])
self.assertLessEqual(
len(msg.params[2]),
kicklen,
f"Server sent kick reason longer than KICKLEN {kicklen}",
)

# Rejoin and kick with a reason longer than the limit
self.getMessages(1)
self.getMessages(2)
self.sendLine(2, "JOIN #test")
self.getMessages(2)
self.getMessages(1)

long_reason = "b" * (kicklen + 50)
self.sendLine(1, f"KICK #test bar :{long_reason}")
msg = self.getMessage(1)
self.assertMessageMatch(msg, command="KICK", params=["#test", "bar", ANYSTR])
self.assertLessEqual(
len(msg.params[2]),
kicklen,
f"Server did not truncate kick reason to KICKLEN {kicklen}",
)
45 changes: 44 additions & 1 deletion irctest/server_tests/topic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from irctest import cases, client_mock, runner
from irctest.numerics import ERR_CHANOPRIVSNEEDED, RPL_NOTOPIC, RPL_TOPIC, RPL_TOPICTIME
from irctest.patma import ANYSTR


class TopicTestCase(cases.BaseServerTestCase):
Expand Down Expand Up @@ -172,6 +173,48 @@ def testUnsetTopicResponses(self):
# topic is once again unset, shouldn't send RPL_NOTOPIC on initial join
self.assertNotIn(RPL_NOTOPIC, [m.command for m in messages])

@cases.mark_specifications("Modern")
@cases.mark_isupport("TOPICLEN")
def testTopiclen(self):
"""
"TOPICLEN=<number>
The TOPICLEN parameter indicates the maximum length of a topic that a client may set on a channel."
-- https://modern.ircdocs.horse/#topiclen-parameter
"""
self.connectClient("foo")
self.joinChannel(1, "#test")

if "TOPICLEN" not in self.server_support:
raise runner.IsupportTokenNotSupported("TOPICLEN")

topiclen = int(self.server_support["TOPICLEN"])

# Set a topic at exactly the limit
valid_topic = "a" * topiclen
self.sendLine(1, f"TOPIC #test :{valid_topic}")
self.assertMessageMatch(
self.getMessage(1), command="TOPIC", params=["#test", valid_topic]
)

self.connectClient("bar")
self.sendLine(2, "JOIN #test")
(msg,) = [msg for msg in self.getMessages(2) if msg.command == RPL_TOPIC]
self.assertMessageMatch(
msg, command=RPL_TOPIC, params=["bar", "#test", valid_topic]
)

# Try a topic longer than the limit
self.getMessages(1) # clear
long_topic = "b" * (topiclen + 50)
self.sendLine(1, f"TOPIC #test :{long_topic}")
msg = self.getMessage(1)
self.assertMessageMatch(msg, command="TOPIC", params=["#test", ANYSTR])
self.assertLessEqual(
len(msg.params[1]),
topiclen,
f"Server set topic longer than TOPICLEN {topiclen}",
)


class TopicPrivilegesTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC2812")
Expand Down Expand Up @@ -214,7 +257,7 @@ def testTopicPrivileges(self):
self.connectClient("buzz", name="buzz")
self.sendLine("buzz", "JOIN #chan")
replies = self.getMessages("buzz")
rpl_topic = [msg for msg in replies if msg.command == RPL_TOPIC][0]
(rpl_topic,) = [msg for msg in replies if msg.command == RPL_TOPIC]
self.assertMessageMatch(
rpl_topic, command=RPL_TOPIC, params=["buzz", "#chan", "new topic"]
)
Expand Down
8 changes: 7 additions & 1 deletion irctest/specifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,19 @@ def from_name(cls, name: str) -> Capabilities:

@enum.unique
class IsupportTokens(enum.Enum):
AWAYLEN = "AWAYLEN"
BOT = "BOT"
CHANLIMIT = "CHANLIMIT"
CHANNELLEN = "CHANNELLEN"
ELIST = "ELIST"
INVEX = "INVEX"
PREFIX = "PREFIX"
KICKLEN = "KICKLEN"
MONITOR = "MONITOR"
NICKLEN = "NICKLEN"
PREFIX = "PREFIX"
STATUSMSG = "STATUSMSG"
TARGMAX = "TARGMAX"
TOPICLEN = "TOPICLEN"
UTF8ONLY = "UTF8ONLY"
WHOX = "WHOX"

Expand Down
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,19 @@ markers =
sts

# isupport tokens
AWAYLEN
BOT
CHANLIMIT
CHANNELLEN
ELIST
INVEX
KICKLEN
MONITOR
NICKLEN
PREFIX
STATUSMSG
TARGMAX
TOPICLEN
UTF8ONLY
WHOX

Expand Down
Loading