From a1324407893b603fe6b55ce7c4ee385938291ae1 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 7 Jul 2024 08:33:48 +0200 Subject: [PATCH] add various channel mode tests (#276) --- .github/workflows/test-stable.yml | 2 +- irctest/controllers/ngircd.py | 3 + irctest/server_tests/chmodes/modeis.py | 67 +++++++++++ irctest/server_tests/chmodes/operator.py | 147 +++++++++++++++++++++++ workflows.yml | 2 +- 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 irctest/server_tests/chmodes/modeis.py create mode 100644 irctest/server_tests/chmodes/operator.py diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index dfe76569..e09d1c7f 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -189,7 +189,7 @@ jobs: uses: actions/checkout@v4 with: path: inspircd - ref: v3.17.0 + ref: v3.17.1 repository: inspircd/inspircd - name: Build InspIRCd run: | diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index 7b1a3ec1..460bba78 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -28,6 +28,9 @@ [Operator] Name = operuser Password = operpassword + +[Limits] + MaxNickLength = 32 # defaults to 9 """ diff --git a/irctest/server_tests/chmodes/modeis.py b/irctest/server_tests/chmodes/modeis.py new file mode 100644 index 00000000..e1df129f --- /dev/null +++ b/irctest/server_tests/chmodes/modeis.py @@ -0,0 +1,67 @@ +from irctest import cases +from irctest.numerics import RPL_CHANNELCREATED, RPL_CHANNELMODEIS +from irctest.patma import ANYSTR, ListRemainder, StrRe + + +class RplChannelModeIsTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("Modern") + def testChannelModeIs(self): + """Test RPL_CHANNELMODEIS and RPL_CHANNELCREATED as responses to + `MODE #channel`: + + + """ + expected_numerics = {RPL_CHANNELMODEIS, RPL_CHANNELCREATED} + if self.controller.software_name in ("irc2", "Sable"): + # irc2 and Sable don't use timestamps for conflict resolution, + # consequently they don't store the channel creation timestamp + # and don't send RPL_CHANNELCREATED + expected_numerics = {RPL_CHANNELMODEIS} + + self.connectClient("chanop", name="chanop") + self.joinChannel("chanop", "#chan") + # i, n, and t are specified by RFC1459; some of them may be on by default, + # but after this, at least those three should be enabled: + self.sendLine("chanop", "MODE #chan +int") + self.getMessages("chanop") + + self.sendLine("chanop", "MODE #chan") + messages = self.getMessages("chanop") + self.assertEqual(expected_numerics, {msg.command for msg in messages}) + for message in messages: + if message.command == RPL_CHANNELMODEIS: + # the final parameters are the mode string (e.g. `+int`), + # and then optionally any mode parameters (in case the ircd + # lists a mode that takes a parameter) + self.assertMessageMatch( + message, + command=RPL_CHANNELMODEIS, + params=["chanop", "#chan", ListRemainder(ANYSTR, min_length=1)], + ) + final_param = message.params[2] + self.assertEqual(final_param[0], "+") + enabled_modes = list(final_param[1:]) + break + + self.assertLessEqual({"i", "n", "t"}, set(enabled_modes)) + + # remove all the modes listed by RPL_CHANNELMODEIS + self.sendLine("chanop", f"MODE #chan -{''.join(enabled_modes)}") + response = self.getMessage("chanop") + # we should get something like: MODE #chan -int + self.assertMessageMatch( + response, command="MODE", params=["#chan", StrRe("^-.*")] + ) + self.assertEqual(set(response.params[1][1:]), set(enabled_modes)) + + self.sendLine("chanop", "MODE #chan") + messages = self.getMessages("chanop") + self.assertEqual(expected_numerics, {msg.command for msg in messages}) + # all modes have been disabled; the correct representation of this is `+` + for message in messages: + if message.command == RPL_CHANNELMODEIS: + self.assertMessageMatch( + message, + command=RPL_CHANNELMODEIS, + params=["chanop", "#chan", "+"], + ) diff --git a/irctest/server_tests/chmodes/operator.py b/irctest/server_tests/chmodes/operator.py new file mode 100644 index 00000000..a4b04658 --- /dev/null +++ b/irctest/server_tests/chmodes/operator.py @@ -0,0 +1,147 @@ +from irctest import cases +from irctest.numerics import ( + ERR_CHANOPRIVSNEEDED, + ERR_NOSUCHCHANNEL, + ERR_NOSUCHNICK, + ERR_NOTONCHANNEL, + ERR_USERNOTINCHANNEL, +) + + +class ChannelOperatorModeTestCase(cases.BaseServerTestCase): + """Test various error and success cases around the channel operator mode: + + + """ + + def setupNicks(self): + """Set up a standard set of three nicknames and two channels + for testing channel-user MODE interactions.""" + # first nick to join the channel is privileged: + self.connectClient("chanop", name="chanop") + self.joinChannel("chanop", "#chan") + + self.connectClient("unprivileged", name="unprivileged") + self.joinChannel("unprivileged", "#chan") + self.getMessages("chanop") + + self.connectClient("unrelated", name="unrelated") + self.joinChannel("unrelated", "#unrelated") + self.joinChannel("unprivileged", "#unrelated") + self.getMessages("unrelated") + + @cases.mark_specifications("Modern") + @cases.xfailIfSoftware(["irc2"], "broken in irc2") + def testChannelOperatorModeSenderPrivsNeeded(self): + """Test that +o from a channel member without the necessary privileges + fails as expected.""" + self.setupNicks() + # sender is a channel member but without the necessary privileges: + self.sendLine("unprivileged", "MODE #chan +o unprivileged") + messages = self.getMessages("unprivileged") + self.assertEqual(len(messages), 1) + self.assertMessageMatch(messages[0], command=ERR_CHANOPRIVSNEEDED) + + @cases.mark_specifications("Modern") + def testChannelOperatorModeTargetNotInChannel(self): + """Test that +o targeting a user not present in the channel fails + as expected.""" + self.setupNicks() + # sender is a chanop, but target nick is not in the channel: + self.sendLine("chanop", "MODE #chan +o unrelated") + messages = self.getMessages("chanop") + self.assertEqual(len(messages), 1) + self.assertMessageMatch(messages[0], command=ERR_USERNOTINCHANNEL) + + @cases.mark_specifications("Modern") + def testChannelOperatorModeTargetDoesNotExist(self): + """Test that +o targeting a nonexistent nick fails as expected.""" + self.setupNicks() + # sender is a chanop, but target nick does not exist: + self.sendLine("chanop", "MODE #chan +o nobody") + messages = self.getMessages("chanop") + # ERR_NOSUCHNICK is typical, Bahamut additionally sends ERR_USERNOTINCHANNEL + if self.controller.software_name != "Bahamut": + self.assertEqual(len(messages), 1) + self.assertMessageMatch(messages[0], command=ERR_NOSUCHNICK) + else: + self.assertLessEqual(len(messages), 2) + commands = {message.command for message in messages} + self.assertLessEqual({ERR_NOSUCHNICK}, commands) + self.assertLessEqual(commands, {ERR_NOSUCHNICK, ERR_USERNOTINCHANNEL}) + + @cases.mark_specifications("Modern") + def testChannelOperatorModeChannelDoesNotExist(self): + """Test that +o targeting a nonexistent channel fails as expected.""" + self.setupNicks() + # target channel does not exist, but target nick does: + self.sendLine("chanop", "MODE #nonexistentchan +o chanop") + messages = self.getMessages("chanop") + self.assertEqual(len(messages), 1) + # Modern: "If is a channel that does not exist on the network, + # the ERR_NOSUCHCHANNEL (403) numeric is returned." + # However, Unreal and ngircd send 401 ERR_NOSUCHNICK here instead: + if self.controller.software_name not in ("UnrealIRCd", "ngIRCd"): + self.assertEqual(messages[0].command, ERR_NOSUCHCHANNEL) + else: + self.assertIn(messages[0].command, [ERR_NOSUCHCHANNEL, ERR_NOSUCHNICK]) + + @cases.mark_specifications("Modern") + def testChannelOperatorModeChannelAndTargetDoNotExist(self): + """Test that +o targeting a nonexistent channel and nickname + fails as expected.""" + self.setupNicks() + # neither target channel nor target nick exist: + self.sendLine("chanop", "MODE #nonexistentchan +o nobody") + messages = self.getMessages("chanop") + self.assertEqual(len(messages), 1) + self.assertIn( + messages[0].command, + [ERR_NOSUCHCHANNEL, ERR_NOTONCHANNEL, ERR_NOSUCHNICK, ERR_USERNOTINCHANNEL], + ) + + @cases.mark_specifications("Modern") + def testChannelOperatorModeSenderNonMember(self): + """Test that +o where the sender is not a channel member + fails as expected.""" + self.setupNicks() + # sender is not a channel member, target nick exists and is a channel member: + self.sendLine("chanop", "MODE #unrelated +o unprivileged") + messages = self.getMessages("chanop") + self.assertEqual(len(messages), 1) + self.assertIn(messages[0].command, [ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED]) + + @cases.mark_specifications("Modern") + def testChannelOperatorModeSenderAndTargetNonMembers(self): + """Test that +o where neither the sender nor the target is a channel + member fails as expected.""" + self.setupNicks() + # sender is not a channel member, target nick exists but is not a channel member: + self.sendLine("chanop", "MODE #unrelated +o chanop") + messages = self.getMessages("chanop") + self.assertEqual(len(messages), 1) + self.assertIn( + messages[0].command, + [ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED, ERR_USERNOTINCHANNEL], + ) + + @cases.mark_specifications("Modern") + def testChannelOperatorModeSuccess(self): + """Tests a successful grant of +o in a channel.""" + self.setupNicks() + + self.sendLine("chanop", "MODE #chan +o unprivileged") + messages = self.getMessages("chanop") + self.assertEqual(len(messages), 1) + self.assertMessageMatch( + messages[0], + command="MODE", + params=["#chan", "+o", "unprivileged"], + ) + messages = self.getMessages("unprivileged") + self.assertEqual(len(messages), 1) + self.assertMessageMatch( + messages[0], + command="MODE", + params=["#chan", "+o", "unprivileged"], + ) diff --git a/workflows.yml b/workflows.yml index 01cf465e..2a2991ff 100644 --- a/workflows.yml +++ b/workflows.yml @@ -148,7 +148,7 @@ software: name: InspIRCd repository: inspircd/inspircd refs: &inspircd_refs - stable: v3.17.0 + stable: v3.17.1 release: null devel: master devel_release: insp3