Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
265 changes: 265 additions & 0 deletions irctest/server_tests/chmodes/invite_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
"""
Invite exception mode (`Modern
<https://modern.ircdocs.horse/#invite-exception-channel-mode>`__)

The invite exception mode allows channel operators to specify masks of users
who can join an invite-only channel without needing an explicit INVITE.
"""

from irctest import cases, runner
from irctest.numerics import ERR_INVITEONLYCHAN, RPL_ENDOFINVITELIST, RPL_INVITELIST
Comment thread
slingamn marked this conversation as resolved.
Outdated
from irctest.patma import ANYSTR, StrRe


@cases.mark_isupport("INVEX")
class InviteExceptionTestCase(cases.BaseServerTestCase):
def getInviteExceptionMode(self) -> str:
"""Get the invite exception mode letter from ISUPPORT and validate it."""
if self.server_support and "INVEX" in self.server_support:
mode = self.server_support["INVEX"] or "I"
if "CHANMODES" in self.server_support:
chanmodes = self.server_support["CHANMODES"]
if chanmodes:
self.assertIn(
mode,
chanmodes,
fail_msg="ISUPPORT INVEX is present, but '{item}' is missing "
"from 'CHANMODES={list}'",
)
self.assertIn(
mode,
chanmodes.split(",")[0],
fail_msg="ISUPPORT INVEX is present, but '{item}' is not "
"in group A",
)
else:
mode = "I"
if self.server_support and "CHANMODES" in self.server_support:
chanmodes = self.server_support["CHANMODES"]
if chanmodes and "I" not in chanmodes:
raise runner.OptionalExtensionNotSupported(
"Invite exception (or mode letter is not +I)"
)
if chanmodes:
self.assertIn(
mode,
chanmodes.split(",")[0],
fail_msg="Mode +I (assumed to be invite exception) is present, "
"but 'I' is not in group A",
)
else:
raise runner.OptionalExtensionNotSupported("ISUPPORT CHANMODES")
return mode

@cases.mark_specifications("Modern")
def testInviteException(self):
"""Test that invite exception (+I) allows users to bypass invite-only (+i).

https://modern.ircdocs.horse/#invite-exception-channel-mode
"""
self.connectClient("chanop", name="chanop")
mode = self.getInviteExceptionMode()

# Create channel and set invite-only mode
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")

self.sendLine("chanop", "MODE #chan +i")
self.getMessages("chanop")

# User matching no exception should be blocked
self.connectClient("Bar", name="bar")
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command=ERR_INVITEONLYCHAN)

# Set invite exception for bar!*@*
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
self.assertMessageMatch(
self.getMessage("chanop"),
command="MODE",
params=["#chan", f"+{mode}", "bar!*@*"],
)

# User matching the exception should now be able to join
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")

@cases.mark_specifications("Modern")
def testInviteExceptionList(self):
"""Test querying the invite exception list.

"346 RPL_INVEXLIST
"<client> <channel> <mask>"

Sent as a reply to the MODE command, when clients are viewing the current
entries on a channel’s invite-exception list. "
-- https://modern.ircdocs.horse/#rplinvexlist-346

"347 RPL_ENDOFINVEXLIST
"<client> <channel> :End of Channel Invite Exception List"

Sent as a reply to the MODE command, this numeric indicates the end of
a channel’s invite-exception list."
-- https://modern.ircdocs.horse/#rplendofinvexlist-347

Note: Some servers include optional [<who> <set-ts>] parameters
like RPL_BANLIST does.
"""
self.connectClient("chanop", name="chanop")
mode = self.getInviteExceptionMode()

self.joinChannel("chanop", "#chan")
self.getMessages("chanop")

# Set an invite exception
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
self.assertMessageMatch(
self.getMessage("chanop"),
command="MODE",
params=["#chan", f"+{mode}", "bar!*@*"],
)

# Query the invite exception list
self.sendLine("chanop", f"MODE #chan +{mode}")

m = self.getMessage("chanop")
if len(m.params) == 3:
# Old format
self.assertMessageMatch(
m,
command=RPL_INVITELIST,
params=[
"chanop",
"#chan",
"bar!*@*",
],
)
else:
# Modern format with who set it and timestamp
self.assertMessageMatch(
m,
command=RPL_INVITELIST,
params=[
"chanop",
"#chan",
"bar!*@*",
StrRe("chanop(!.*@.*)?"),
StrRe("[0-9]+"),
],
)

self.assertMessageMatch(
self.getMessage("chanop"),
command=RPL_ENDOFINVITELIST,
params=[
"chanop",
"#chan",
ANYSTR,
],
)

@cases.mark_specifications("Modern")
def testInviteExceptionRemoval(self):
self.connectClient("chanop", name="chanop")
mode = self.getInviteExceptionMode()

# Create channel and set invite-only mode with exception
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")

self.sendLine("chanop", "MODE #chan +i")
self.getMessages("chanop")

self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
self.assertMessageMatch(
self.getMessage("chanop"),
command="MODE",
params=["#chan", f"+{mode}", "bar!*@*"],
)

# User can join via exception
self.connectClient("Bar", name="bar")
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")

# User leaves
self.sendLine("bar", "PART #chan")
self.getMessages("bar")
self.getMessages("chanop")

# Remove the exception
self.sendLine("chanop", f"MODE #chan -{mode} bar!*@*")
self.assertMessageMatch(
self.getMessage("chanop"),
command="MODE",
params=["#chan", f"-{mode}", "bar!*@*"],
)

# User should now be blocked
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command=ERR_INVITEONLYCHAN)

@cases.mark_specifications("Modern")
def testInviteExceptionWithoutInviteOnly(self):
self.connectClient("chanop", name="chanop")
mode = self.getInviteExceptionMode()

# Create channel without invite-only mode
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")

# Set invite exception (should be allowed but has no effect)
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
self.assertMessageMatch(
self.getMessage("chanop"),
command="MODE",
params=["#chan", f"+{mode}", "bar!*@*"],
)

# User should be able to join regardless (channel is not +i)
self.connectClient("Baz", name="baz")
self.sendLine("baz", "JOIN #chan")
self.assertMessageMatch(self.getMessage("baz"), command="JOIN")

@cases.mark_specifications("Modern")
def testInviteExceptionMultipleMasks(self):
self.connectClient("chanop", name="chanop")
mode = self.getInviteExceptionMode()

# Create channel and set invite-only mode
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")

self.sendLine("chanop", "MODE #chan +i")
self.getMessages("chanop")

# Set exception for bar!*@* but not baz!*@*
self.sendLine("chanop", f"MODE #chan +{mode} bar!*@*")
self.assertMessageMatch(
self.getMessage("chanop"),
command="MODE",
params=["#chan", f"+{mode}", "bar!*@*"],
)

# bar should be able to join
self.connectClient("Bar", name="bar")
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
self.getMessages("chanop")

# baz should be blocked
self.connectClient("Baz", name="baz")
self.sendLine("baz", "JOIN #chan")
self.assertMessageMatch(self.getMessage("baz"), command=ERR_INVITEONLYCHAN)

# Add exception for baz!*@*
self.sendLine("chanop", f"MODE #chan +{mode} baz!*@*")
self.assertMessageMatch(
self.getMessage("chanop"),
command="MODE",
params=["#chan", f"+{mode}", "baz!*@*"],
)

# baz should now be able to join
self.sendLine("baz", "JOIN #chan")
self.assertMessageMatch(self.getMessage("baz"), command="JOIN")
44 changes: 0 additions & 44 deletions irctest/server_tests/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,50 +410,6 @@ def testInviteList(self):
params=["bar", ANYSTR],
)

@cases.mark_isupport("INVEX")
@cases.mark_specifications("Modern")
def testInvexList(self):
self.connectClient("foo")
self.getMessages(1)

if "INVEX" in self.server_support:
invex = self.server_support.get("INVEX") or "I"
else:
raise runner.IsupportTokenNotSupported("INVEX")

self.sendLine(1, "JOIN #chan")
self.getMessages(1)

self.sendLine(1, f"MODE #chan +{invex} bar!*@*")
self.getMessages(1)

self.sendLine(1, f"MODE #chan +{invex}")
m = self.getMessage(1)
if len(m.params) == 3:
# Old format
self.assertMessageMatch(
m,
command="346",
params=["foo", "#chan", "bar!*@*"],
)
else:
self.assertMessageMatch(
m,
command="346",
params=[
"foo",
"#chan",
"bar!*@*",
StrRe("foo(!.*@.*)?"),
StrRe("[0-9]+"),
],
)
self.assertMessageMatch(
self.getMessage(1),
command="347",
params=["foo", "#chan", ANYSTR],
)

@cases.mark_specifications("Ergo")
def testInviteExemptsFromBan(self):
# regression test for ergochat/ergo#1876;
Expand Down
Loading