Skip to content

Commit 60c8f64

Browse files
authored
Add tests for mode +I (#332)
1 parent 5a7ff3e commit 60c8f64

File tree

3 files changed

+267
-46
lines changed

3 files changed

+267
-46
lines changed

irctest/numerics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@
8080
RPL_WHOISACTUALLY = "338"
8181
RPL_INVITING = "341"
8282
RPL_SUMMONING = "342"
83-
RPL_INVITELIST = "346"
84-
RPL_ENDOFINVITELIST = "347"
83+
RPL_INVEXLIST = "346"
84+
RPL_ENDOFINVEXLIST = "347"
8585
RPL_EXCEPTLIST = "348"
8686
RPL_ENDOFEXCEPTLIST = "349"
8787
RPL_VERSION = "351"
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"""
2+
Invite exception mode (`Modern
3+
<https://modern.ircdocs.horse/#invite-exception-channel-mode>`__)
4+
5+
The invite exception mode allows channel operators to specify masks of users
6+
who can join an invite-only channel without needing an explicit INVITE.
7+
"""
8+
9+
from irctest import cases, runner
10+
from irctest.numerics import ERR_INVITEONLYCHAN, RPL_ENDOFINVEXLIST, RPL_INVEXLIST
11+
from irctest.patma import ANYSTR, StrRe
12+
13+
14+
@cases.mark_isupport("INVEX")
15+
class InviteExceptionTestCase(cases.BaseServerTestCase):
16+
def getInviteExceptionMode(self) -> str:
17+
"""Get the invite exception mode letter from ISUPPORT and validate it."""
18+
if self.server_support and "INVEX" in self.server_support:
19+
mode = self.server_support["INVEX"] or "I"
20+
if "CHANMODES" in self.server_support:
21+
chanmodes = self.server_support["CHANMODES"]
22+
if chanmodes:
23+
self.assertIn(
24+
mode,
25+
chanmodes,
26+
fail_msg="ISUPPORT INVEX is present, but '{item}' is missing "
27+
"from 'CHANMODES={list}'",
28+
)
29+
self.assertIn(
30+
mode,
31+
chanmodes.split(",")[0],
32+
fail_msg="ISUPPORT INVEX is present, but '{item}' is not "
33+
"in group A",
34+
)
35+
else:
36+
mode = "I"
37+
if self.server_support and "CHANMODES" in self.server_support:
38+
chanmodes = self.server_support["CHANMODES"]
39+
if chanmodes and "I" not in chanmodes:
40+
raise runner.OptionalExtensionNotSupported(
41+
"Invite exception (or mode letter is not +I)"
42+
)
43+
if chanmodes:
44+
self.assertIn(
45+
mode,
46+
chanmodes.split(",")[0],
47+
fail_msg="Mode +I (assumed to be invite exception) is present, "
48+
"but 'I' is not in group A",
49+
)
50+
else:
51+
raise runner.OptionalExtensionNotSupported("ISUPPORT CHANMODES")
52+
return mode
53+
54+
@cases.mark_specifications("Modern")
55+
def testInviteException(self):
56+
"""Test that invite exception (+I) allows users to bypass invite-only (+i).
57+
58+
https://modern.ircdocs.horse/#invite-exception-channel-mode
59+
"""
60+
self.connectClient("chanop", name="chanop")
61+
mode = self.getInviteExceptionMode()
62+
63+
# Create channel and set invite-only mode
64+
self.joinChannel("chanop", "#chan")
65+
self.getMessages("chanop")
66+
67+
self.sendLine("chanop", "MODE #chan +i")
68+
self.getMessages("chanop")
69+
70+
# User matching no exception should be blocked
71+
self.connectClient("Bar", name="bar")
72+
self.sendLine("bar", "JOIN #chan")
73+
self.assertMessageMatch(self.getMessage("bar"), command=ERR_INVITEONLYCHAN)
74+
75+
# Set invite exception for bar!*@*
76+
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
77+
self.assertMessageMatch(
78+
self.getMessage("chanop"),
79+
command="MODE",
80+
params=["#chan", f"+{mode}", "bar!*@*"],
81+
)
82+
83+
# User matching the exception should now be able to join
84+
self.sendLine("bar", "JOIN #chan")
85+
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
86+
87+
@cases.mark_specifications("Modern")
88+
def testInviteExceptionList(self):
89+
"""Test querying the invite exception list.
90+
91+
"346 RPL_INVEXLIST
92+
"<client> <channel> <mask>"
93+
94+
Sent as a reply to the MODE command, when clients are viewing the current
95+
entries on a channel’s invite-exception list. "
96+
-- https://modern.ircdocs.horse/#rplinvexlist-346
97+
98+
"347 RPL_ENDOFINVEXLIST
99+
"<client> <channel> :End of Channel Invite Exception List"
100+
101+
Sent as a reply to the MODE command, this numeric indicates the end of
102+
a channel’s invite-exception list."
103+
-- https://modern.ircdocs.horse/#rplendofinvexlist-347
104+
105+
Note: Some servers include optional [<who> <set-ts>] parameters
106+
like RPL_BANLIST does.
107+
"""
108+
self.connectClient("chanop", name="chanop")
109+
mode = self.getInviteExceptionMode()
110+
111+
self.joinChannel("chanop", "#chan")
112+
self.getMessages("chanop")
113+
114+
# Set an invite exception
115+
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
116+
self.assertMessageMatch(
117+
self.getMessage("chanop"),
118+
command="MODE",
119+
params=["#chan", f"+{mode}", "bar!*@*"],
120+
)
121+
122+
# Query the invite exception list
123+
self.sendLine("chanop", f"MODE #chan +{mode}")
124+
125+
m = self.getMessage("chanop")
126+
if len(m.params) == 3:
127+
# Old format
128+
self.assertMessageMatch(
129+
m,
130+
command=RPL_INVEXLIST,
131+
params=[
132+
"chanop",
133+
"#chan",
134+
"bar!*@*",
135+
],
136+
)
137+
else:
138+
# Modern format with who set it and timestamp
139+
self.assertMessageMatch(
140+
m,
141+
command=RPL_INVEXLIST,
142+
params=[
143+
"chanop",
144+
"#chan",
145+
"bar!*@*",
146+
StrRe("chanop(!.*@.*)?"),
147+
StrRe("[0-9]+"),
148+
],
149+
)
150+
151+
self.assertMessageMatch(
152+
self.getMessage("chanop"),
153+
command=RPL_ENDOFINVEXLIST,
154+
params=[
155+
"chanop",
156+
"#chan",
157+
ANYSTR,
158+
],
159+
)
160+
161+
@cases.mark_specifications("Modern")
162+
def testInviteExceptionRemoval(self):
163+
self.connectClient("chanop", name="chanop")
164+
mode = self.getInviteExceptionMode()
165+
166+
# Create channel and set invite-only mode with exception
167+
self.joinChannel("chanop", "#chan")
168+
self.getMessages("chanop")
169+
170+
self.sendLine("chanop", "MODE #chan +i")
171+
self.getMessages("chanop")
172+
173+
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
174+
self.assertMessageMatch(
175+
self.getMessage("chanop"),
176+
command="MODE",
177+
params=["#chan", f"+{mode}", "bar!*@*"],
178+
)
179+
180+
# User can join via exception
181+
self.connectClient("Bar", name="bar")
182+
self.sendLine("bar", "JOIN #chan")
183+
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
184+
185+
# User leaves
186+
self.sendLine("bar", "PART #chan")
187+
self.getMessages("bar")
188+
self.getMessages("chanop")
189+
190+
# Remove the exception
191+
self.sendLine("chanop", f"MODE #chan -{mode} bar!*@*")
192+
self.assertMessageMatch(
193+
self.getMessage("chanop"),
194+
command="MODE",
195+
params=["#chan", f"-{mode}", "bar!*@*"],
196+
)
197+
198+
# User should now be blocked
199+
self.sendLine("bar", "JOIN #chan")
200+
self.assertMessageMatch(self.getMessage("bar"), command=ERR_INVITEONLYCHAN)
201+
202+
@cases.mark_specifications("Modern")
203+
def testInviteExceptionWithoutInviteOnly(self):
204+
self.connectClient("chanop", name="chanop")
205+
mode = self.getInviteExceptionMode()
206+
207+
# Create channel without invite-only mode
208+
self.joinChannel("chanop", "#chan")
209+
self.getMessages("chanop")
210+
211+
# Set invite exception (should be allowed but has no effect)
212+
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
213+
self.assertMessageMatch(
214+
self.getMessage("chanop"),
215+
command="MODE",
216+
params=["#chan", f"+{mode}", "bar!*@*"],
217+
)
218+
219+
# User should be able to join regardless (channel is not +i)
220+
self.connectClient("Baz", name="baz")
221+
self.sendLine("baz", "JOIN #chan")
222+
self.assertMessageMatch(self.getMessage("baz"), command="JOIN")
223+
224+
@cases.mark_specifications("Modern")
225+
def testInviteExceptionMultipleMasks(self):
226+
self.connectClient("chanop", name="chanop")
227+
mode = self.getInviteExceptionMode()
228+
229+
# Create channel and set invite-only mode
230+
self.joinChannel("chanop", "#chan")
231+
self.getMessages("chanop")
232+
233+
self.sendLine("chanop", "MODE #chan +i")
234+
self.getMessages("chanop")
235+
236+
# Set exception for bar!*@* but not baz!*@*
237+
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
238+
self.assertMessageMatch(
239+
self.getMessage("chanop"),
240+
command="MODE",
241+
params=["#chan", f"+{mode}", "bar!*@*"],
242+
)
243+
244+
# bar should be able to join
245+
self.connectClient("Bar", name="bar")
246+
self.sendLine("bar", "JOIN #chan")
247+
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
248+
self.getMessages("chanop")
249+
250+
# baz should be blocked
251+
self.connectClient("Baz", name="baz")
252+
self.sendLine("baz", "JOIN #chan")
253+
self.assertMessageMatch(self.getMessage("baz"), command=ERR_INVITEONLYCHAN)
254+
255+
# Add exception for baz!*@*
256+
self.sendLine("chanop", f"MODE #chan +{mode} baz!*@*")
257+
self.assertMessageMatch(
258+
self.getMessage("chanop"),
259+
command="MODE",
260+
params=["#chan", f"+{mode}", "baz!*@*"],
261+
)
262+
263+
# baz should now be able to join
264+
self.sendLine("baz", "JOIN #chan")
265+
self.assertMessageMatch(self.getMessage("baz"), command="JOIN")

irctest/server_tests/invite.py

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -410,50 +410,6 @@ def testInviteList(self):
410410
params=["bar", ANYSTR],
411411
)
412412

413-
@cases.mark_isupport("INVEX")
414-
@cases.mark_specifications("Modern")
415-
def testInvexList(self):
416-
self.connectClient("foo")
417-
self.getMessages(1)
418-
419-
if "INVEX" in self.server_support:
420-
invex = self.server_support.get("INVEX") or "I"
421-
else:
422-
raise runner.IsupportTokenNotSupported("INVEX")
423-
424-
self.sendLine(1, "JOIN #chan")
425-
self.getMessages(1)
426-
427-
self.sendLine(1, f"MODE #chan +{invex} bar!*@*")
428-
self.getMessages(1)
429-
430-
self.sendLine(1, f"MODE #chan +{invex}")
431-
m = self.getMessage(1)
432-
if len(m.params) == 3:
433-
# Old format
434-
self.assertMessageMatch(
435-
m,
436-
command="346",
437-
params=["foo", "#chan", "bar!*@*"],
438-
)
439-
else:
440-
self.assertMessageMatch(
441-
m,
442-
command="346",
443-
params=[
444-
"foo",
445-
"#chan",
446-
"bar!*@*",
447-
StrRe("foo(!.*@.*)?"),
448-
StrRe("[0-9]+"),
449-
],
450-
)
451-
self.assertMessageMatch(
452-
self.getMessage(1),
453-
command="347",
454-
params=["foo", "#chan", ANYSTR],
455-
)
456-
457413
@cases.mark_specifications("Ergo")
458414
def testInviteExemptsFromBan(self):
459415
# regression test for ergochat/ergo#1876;

0 commit comments

Comments
 (0)