Skip to content

Commit 27742ed

Browse files
committed
Add tests for length limits in ISUPPORT
1 parent 4fb6743 commit 27742ed

6 files changed

Lines changed: 271 additions & 2 deletions

File tree

irctest/server_tests/away.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
`Modern <https://modern.ircdocs.horse/#away-message>`__)
44
"""
55

6-
from irctest import cases
6+
from irctest import cases, runner
77
from irctest.numerics import (
88
RPL_AWAY,
99
RPL_NOWAWAY,
@@ -181,3 +181,45 @@ def testAwayEmptyMessage(self):
181181
replies = self.getMessages("qux")
182182
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
183183
self.assertNotIn(RPL_AWAY, [msg.command for msg in replies])
184+
185+
@cases.mark_specifications("Modern")
186+
@cases.mark_isupport("AWAYLEN")
187+
def testAwaylen(self):
188+
"""
189+
"AWAYLEN=<number>
190+
The AWAYLEN parameter indicates the maximum length for the <reason> of an AWAY command."
191+
-- https://modern.ircdocs.horse/#awaylen-parameter
192+
"""
193+
self.connectClient("foo")
194+
195+
if "AWAYLEN" not in self.server_support:
196+
raise runner.IsupportTokenNotSupported("AWAYLEN")
197+
198+
awaylen = int(self.server_support["AWAYLEN"])
199+
200+
# Set away message at exactly the limit
201+
valid_away = "a" * awaylen
202+
self.sendLine(1, f"AWAY :{valid_away}")
203+
self.assertMessageMatch(
204+
self.getMessage(1), command="306", params=["foo", ANYSTR]
205+
) # RPL_NOWAWAY
206+
207+
# Set away message longer than the limit - should be truncated
208+
long_away = "b" * (awaylen + 50)
209+
self.sendLine(1, f"AWAY :{long_away}")
210+
self.getMessages(1)
211+
212+
# Check the away message
213+
self.connectClient("bar")
214+
self.sendLine(2, "WHOIS foo")
215+
msgs = self.getMessages(2)
216+
217+
away_msgs = [m for m in msgs if m.command == "301"]
218+
self.assertMessageMatch(
219+
away_msgs[0], command="301", params=["bar", "foo", ANYSTR]
220+
)
221+
self.assertLessEqual(
222+
len(away_msgs[0].params[2]),
223+
awaylen,
224+
f"Server sent away message longer than AWAYLEN {awaylen}",
225+
)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Tests for ISUPPORT limit enforcement (`Modern
3+
<https://modern.ircdocs.horse/#rplisupport-parameters>`__)
4+
"""
5+
6+
import pytest
7+
8+
from irctest import cases, runner
9+
from irctest.numerics import (
10+
ERR_ERRONEUSNICKNAME,
11+
ERR_NOSUCHCHANNEL,
12+
ERR_TOOMANYCHANNELS,
13+
)
14+
from irctest.patma import ANYSTR
15+
16+
17+
class IsupportLimitTestCase(cases.BaseServerTestCase):
18+
@cases.mark_specifications("Modern")
19+
@cases.mark_isupport("CHANLIMIT")
20+
@pytest.mark.parametrize("prefix", ["#", "&"])
21+
def testChanlimit(self, prefix):
22+
"""
23+
"CHANLIMIT=<prefixes>:[limit]{,<prefixes>:[limit]}"
24+
-- https://modern.ircdocs.horse/#chanlimit-parameter
25+
"""
26+
self.connectClient("foo")
27+
28+
if "CHANLIMIT" not in self.server_support:
29+
raise runner.IsupportTokenNotSupported("CHANLIMIT")
30+
31+
pairs = [
32+
part.split(":", 1) for part in self.server_support["CHANLIMIT"].split(",")
33+
]
34+
limits = {pair[0]: int(pair[1]) if pair[1] else None for pair in pairs}
35+
36+
self.assertTrue(limits, "CHANLIMIT is empty")
37+
38+
limit = limits.get(prefix)
39+
if limit is None:
40+
raise runner.ImplementationChoice(f"No limit for {prefix} channels")
41+
42+
# Join up to the limit
43+
for i in range(limit):
44+
self.sendLine(1, f"JOIN {prefix}chan{i}")
45+
self.assertMessageMatch(
46+
self.getMessage(1),
47+
command="JOIN",
48+
params=[f"{prefix}chan{i}"],
49+
fail_msg=f"Failed to join channel {i + 1}/{limit}",
50+
)
51+
self.getMessages(1) # clear any remaining messages
52+
53+
# Try to join one more - should fail
54+
self.sendLine(1, f"JOIN {prefix}chan")
55+
self.assertMessageMatch(
56+
self.getMessage(1),
57+
command=ERR_TOOMANYCHANNELS,
58+
params=["foo", f"{prefix}chan", ANYSTR],
59+
)
60+
61+
@cases.mark_specifications("Modern")
62+
@cases.mark_isupport("CHANNELLEN")
63+
@pytest.mark.parametrize("prefix", ["#", "&"])
64+
def testChannellen(self, prefix):
65+
"""
66+
"CHANNELLEN=<number>
67+
The CHANNELLEN parameter specifies the maximum length of a channel name that a client may join."
68+
-- https://modern.ircdocs.horse/#channellen-parameter
69+
"""
70+
self.connectClient("foo")
71+
72+
if "CHANNELLEN" not in self.server_support:
73+
raise runner.IsupportTokenNotSupported("CHANNELLEN")
74+
75+
chantypes = self.server_support.get("CHANTYPES", "#")
76+
if prefix not in chantypes:
77+
raise runner.NotImplementedByController(
78+
f"Server does not support {prefix} channels"
79+
)
80+
81+
channellen = int(self.server_support["CHANNELLEN"])
82+
83+
# Try a channel name at exactly the limit
84+
valid_chan = prefix + "a" * (channellen - 1)
85+
self.sendLine(1, f"JOIN {valid_chan}")
86+
self.assertMessageMatch(self.getMessage(1), command="JOIN", params=[valid_chan])
87+
88+
# Try a channel name longer than the limit
89+
self.getMessages(1) # clear
90+
invalid_chan = prefix + "b" * channellen
91+
self.sendLine(1, f"JOIN {invalid_chan}")
92+
self.assertMessageMatch(
93+
self.getMessage(1),
94+
command=ERR_NOSUCHCHANNEL,
95+
params=["foo", invalid_chan, ANYSTR],
96+
)
97+
98+
@cases.mark_specifications("Modern")
99+
@cases.mark_isupport("NICKLEN")
100+
def testNicklen(self):
101+
"""
102+
"NICKLEN=<number>
103+
"The NICKLEN parameter indicates the maximum length of a nickname that a client may set.
104+
Clients on the network MAY have longer nicks than this.
105+
The value MUST be specified and MUST be a positive integer.
106+
30 or 31 are typical values for this parameter advertised by servers today."
107+
-- https://modern.ircdocs.horse/#nicklen-parameter
108+
"""
109+
self.connectClient("foo")
110+
111+
if "NICKLEN" not in self.server_support:
112+
raise runner.IsupportTokenNotSupported("NICKLEN")
113+
114+
nicklen = int(self.server_support["NICKLEN"])
115+
116+
# Try a nick at exactly the limit
117+
valid_nick = "a" * nicklen
118+
self.sendLine(1, f"NICK {valid_nick}")
119+
self.assertMessageMatch(self.getMessage(1), command="NICK", params=[valid_nick])
120+
121+
# Try a nick longer than the limit
122+
invalid_nick = "b" * (nicklen + 5)
123+
self.sendLine(1, f"NICK {invalid_nick}")
124+
self.assertMessageMatch(
125+
self.getMessage(1),
126+
command=ERR_ERRONEUSNICKNAME,
127+
params=[valid_nick, invalid_nick, ANYSTR],
128+
)

irctest/server_tests/kick.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,54 @@ def testDoubleKickMessages(self, multiple_targets):
272272
m1.params[1], m2.params[1]
273273
)
274274
)
275+
276+
@cases.mark_specifications("Modern")
277+
@cases.mark_isupport("KICKLEN")
278+
def testKicklen(self):
279+
"""
280+
"KICKLEN=<number>
281+
The KICKLEN parameter indicates the maximum length for the <reason> of a KICK command."
282+
-- https://modern.ircdocs.horse/#kicklen-parameter
283+
"""
284+
self.connectClient("foo")
285+
self.joinChannel(1, "#test")
286+
287+
self.connectClient("bar")
288+
self.joinChannel(2, "#test")
289+
290+
if "KICKLEN" not in self.server_support:
291+
raise runner.IsupportTokenNotSupported("KICKLEN")
292+
293+
kicklen = int(self.server_support["KICKLEN"])
294+
295+
# Kick with a reason at exactly the limit
296+
valid_reason = "a" * kicklen
297+
self.sendLine(1, f"KICK #test bar :{valid_reason}")
298+
msg = self.getMessage(1)
299+
300+
if msg.command != "KICK":
301+
raise runner.ImplementationChoice("User doesn't have permission to kick")
302+
303+
self.assertMessageMatch(msg, command="KICK", params=["#test", "bar", ANYSTR])
304+
self.assertLessEqual(
305+
len(msg.params[2]),
306+
kicklen,
307+
f"Server sent kick reason longer than KICKLEN {kicklen}",
308+
)
309+
310+
# Rejoin and kick with a reason longer than the limit
311+
self.getMessages(1)
312+
self.getMessages(2)
313+
self.sendLine(2, "JOIN #test")
314+
self.getMessages(2)
315+
self.getMessages(1)
316+
317+
long_reason = "b" * (kicklen + 50)
318+
self.sendLine(1, f"KICK #test bar :{long_reason}")
319+
msg = self.getMessage(1)
320+
self.assertMessageMatch(msg, command="KICK", params=["#test", "bar", ANYSTR])
321+
self.assertLessEqual(
322+
len(msg.params[2]),
323+
kicklen,
324+
f"Server truncated kick reason to KICKLEN {kicklen}",
325+
)

irctest/server_tests/topic.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from irctest import cases, client_mock, runner
99
from irctest.numerics import ERR_CHANOPRIVSNEEDED, RPL_NOTOPIC, RPL_TOPIC, RPL_TOPICTIME
10+
from irctest.patma import ANYSTR
1011

1112

1213
class TopicTestCase(cases.BaseServerTestCase):
@@ -221,3 +222,38 @@ def testTopicPrivileges(self):
221222
self.assertEqual(
222223
len([msg for msg in replies if msg.command == RPL_TOPICTIME]), 1
223224
)
225+
226+
@cases.mark_specifications("Modern")
227+
@cases.mark_isupport("TOPICLEN")
228+
def testTopiclen(self):
229+
"""
230+
"TOPICLEN=<number>
231+
The TOPICLEN parameter indicates the maximum length of a topic that a client may set on a channel."
232+
-- https://modern.ircdocs.horse/#topiclen-parameter
233+
"""
234+
self.connectClient("foo")
235+
self.joinChannel(1, "#test")
236+
237+
if "TOPICLEN" not in self.server_support:
238+
raise runner.IsupportTokenNotSupported("TOPICLEN")
239+
240+
topiclen = int(self.server_support["TOPICLEN"])
241+
242+
# Set a topic at exactly the limit
243+
valid_topic = "a" * topiclen
244+
self.sendLine(1, f"TOPIC #test :{valid_topic}")
245+
self.assertMessageMatch(
246+
self.getMessage(1), command="TOPIC", params=["#test", valid_topic]
247+
)
248+
249+
# Try a topic longer than the limit
250+
self.getMessages(1) # clear
251+
long_topic = "b" * (topiclen + 50)
252+
self.sendLine(1, f"TOPIC #test :{long_topic}")
253+
msg = self.getMessage(1)
254+
self.assertMessageMatch(msg, command="TOPIC", params=["#test", ANYSTR])
255+
self.assertLessEqual(
256+
len(msg.params[1]),
257+
topiclen,
258+
f"Server set topic longer than TOPICLEN {topiclen}",
259+
)

irctest/specifications.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,19 @@ def from_name(cls, name: str) -> Capabilities:
5353

5454
@enum.unique
5555
class IsupportTokens(enum.Enum):
56+
AWAYLEN = "AWAYLEN"
5657
BOT = "BOT"
58+
CHANLIMIT = "CHANLIMIT"
59+
CHANNELLEN = "CHANNELLEN"
5760
ELIST = "ELIST"
5861
INVEX = "INVEX"
59-
PREFIX = "PREFIX"
62+
KICKLEN = "KICKLEN"
6063
MONITOR = "MONITOR"
64+
NICKLEN = "NICKLEN"
65+
PREFIX = "PREFIX"
6166
STATUSMSG = "STATUSMSG"
6267
TARGMAX = "TARGMAX"
68+
TOPICLEN = "TOPICLEN"
6369
UTF8ONLY = "UTF8ONLY"
6470
WHOX = "WHOX"
6571

pytest.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,19 @@ markers =
3636
sts
3737

3838
# isupport tokens
39+
AWAYLEN
3940
BOT
41+
CHANLIMIT
42+
CHANNELLEN
4043
ELIST
4144
INVEX
45+
KICKLEN
4246
MONITOR
47+
NICKLEN
4348
PREFIX
4449
STATUSMSG
4550
TARGMAX
51+
TOPICLEN
4652
UTF8ONLY
4753
WHOX
4854

0 commit comments

Comments
 (0)