From da8a6d1f98e5a8d3fbc4b45a26a841de4e68f13f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 11 Dec 2021 23:55:57 +0100 Subject: [PATCH] Initial tests for named-modes. Only tested with my Insp module. --- .github/workflows/test-devel.yml | 1 + .github/workflows/test-devel_release.yml | 1 + .github/workflows/test-stable.yml | 1 + README.md | 3 + irctest/controllers/inspircd.py | 1 + irctest/numerics.py | 6 + irctest/server_tests/named_modes.py | 342 +++++++++++++++++++++++ irctest/specifications.py | 1 + pytest.ini | 1 + workflows.yml | 1 + 10 files changed, 358 insertions(+) create mode 100644 irctest/server_tests/named_modes.py diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index f3c4d38..ab1db60 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -145,6 +145,7 @@ jobs: run: | cd $GITHUB_WORKSPACE/inspircd/ patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 92fc3cc..1cebe0e 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -58,6 +58,7 @@ jobs: run: | cd $GITHUB_WORKSPACE/inspircd/ patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 1df34a6..d6019ad 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -185,6 +185,7 @@ jobs: run: | cd $GITHUB_WORKSPACE/inspircd/ patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install diff --git a/README.md b/README.md index c5503af..da10bb7 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,9 @@ cd inspircd # optional, makes tests run considerably faster patch src/inspircd.cpp < ~/irctest/inspircd_mainloop.patch +# third-party module, used in named-modes tests because the spec is not implemented upstream +wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp + ./configure --prefix=$HOME/.local/ --development make -j 4 make install diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 1cd40ba..c7d60c0 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -70,6 +70,7 @@ TEMPLATE_CONFIG = """ + # third-party, https://github.com/progval/inspircd-contrib/blob/namedmodes/4.0/m_ircv3_namedmodes.cpp # for testing mute extbans diff --git a/irctest/numerics.py b/irctest/numerics.py index 6193243..0424891 100644 --- a/irctest/numerics.py +++ b/irctest/numerics.py @@ -201,5 +201,11 @@ ERR_ACCOUNT_INVALID_VERIFY_CODE = "925" RPL_REG_VERIFICATION_REQUIRED = "927" ERR_REG_INVALID_CRED_TYPE = "928" ERR_REG_INVALID_CALLBACK = "929" +RPL_ENDOFPROPLIST = "960" +RPL_PROPLIST = "961" +RPL_ENDOFLISTPROPLIST = "962" +RPL_LISTPROPLIST = "963" +RPL_CHMODELIST = "964" +RPL_UMODELIST = "965" ERR_TOOMANYLANGUAGES = "981" ERR_NOLANGUAGE = "982" diff --git a/irctest/server_tests/named_modes.py b/irctest/server_tests/named_modes.py new file mode 100644 index 0000000..8cdef5b --- /dev/null +++ b/irctest/server_tests/named_modes.py @@ -0,0 +1,342 @@ +import re + +from irctest import cases +from irctest.numerics import ( + ERR_BANNEDFROMCHAN, + ERR_CANNOTSENDTOCHAN, + ERR_INVITEONLYCHAN, + RPL_CHMODELIST, + RPL_ENDOFLISTPROPLIST, + RPL_ENDOFPROPLIST, + RPL_LISTPROPLIST, + RPL_PROPLIST, + RPL_UMODELIST, +) +from irctest.patma import ANYLIST, ANYSTR, ListRemainder, StrRe + + +class _NamedModeTestMixin: + @cases.mark_capabilities("draft/named-modes") + @cases.mark_specifications("IRCv3") + def testListMode(self): + """Checks list modes (type 1), using 'ban' as example.""" + self.connectClient("foo", name="user", capabilities=["draft/named-modes"]) + self.connectClient("chanop", name="chanop", capabilities=["draft/named-modes"]) + self.joinChannel("chanop", "#chan") + self.getMessages("chanop") + + # Set ban + self.sendLine("chanop", "PROP #chan +ban=foo!*@*") + msg = self.getMessage("chanop") + if self.ALLOW_MODE_REPLY and msg.command == "MODE": + self.assertMessageMatch( + msg, command="MODE", params=["#chan", "+b", "foo!*@*"] + ) + else: + self.assertMessageMatch( + msg, command="PROP", params=["#chan", "+ban=foo!*@*"] + ) + + # Should not appear in the main list + self.sendLine("chanop", "PROP #chan") + msg = self.getMessage("chanop") + self.assertMessageMatch( + msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST] + ) + self.assertNotIn("ban", msg.params[2:]) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFPROPLIST, + params=["chanop", "#chan", ANYSTR], + ) + + # Check banned + self.sendLine("chanop", "PROP #chan ban") + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_LISTPROPLIST, + params=["chanop", "#chan", "ban", "foo!*@*", *ANYLIST], + ) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFLISTPROPLIST, + params=["chanop", "#chan", "ban", ANYSTR], + ) + self.sendLine("user", "JOIN #chan") + self.assertMessageMatch(self.getMessage("user"), command=ERR_BANNEDFROMCHAN) + + # Unset ban + self.sendLine("chanop", "PROP #chan -ban=foo!*@*") + msg = self.getMessage("chanop") + if msg.command == "MODE": + self.assertMessageMatch( + msg, command="MODE", params=["#chan", "-b", "foo!*@*"] + ) + else: + self.assertMessageMatch( + msg, command="PROP", params=["#chan", "-ban=foo!*@*"] + ) + + # Check unbanned + self.sendLine("chanop", "PROP #chan ban") + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFLISTPROPLIST, + params=["chanop", "#chan", "ban", ANYSTR], + ) + self.sendLine("user", "JOIN #chan") + self.assertMessageMatch( + self.getMessage("user"), command="JOIN", params=["#chan"] + ) + + @cases.mark_capabilities("draft/named-modes") + @cases.mark_specifications("IRCv3") + def testFlagModeDefaultOn(self): + """Checks list modes (type 1), using 'noextmsg' as example.""" + self.connectClient("foo", name="user", capabilities=["draft/named-modes"]) + self.connectClient("chanop", name="chanop", capabilities=["draft/named-modes"]) + self.joinChannel("chanop", "#chan") + self.getMessages("chanop") + + # Check set + self.sendLine("chanop", "PROP #chan") + msg = self.getMessage("chanop") + self.assertMessageMatch( + msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST] + ) + self.assertIn("noextmsg", msg.params[2:]) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFPROPLIST, + params=["chanop", "#chan", ANYSTR], + ) + self.sendLine("user", "PRIVMSG #chan :hi") + self.assertMessageMatch( + self.getMessage("user"), + command=ERR_CANNOTSENDTOCHAN, + params=["foo", "#chan", ANYSTR], + ) + self.assertEqual(self.getMessages("chanop"), []) + + # Unset + self.sendLine("chanop", "PROP #chan -noextmsg") + msg = self.getMessage("chanop") + if self.ALLOW_MODE_REPLY and msg.command == "MODE": + self.assertMessageMatch(msg, command="MODE", params=["#chan", "-noextmsg"]) + else: + self.assertMessageMatch(msg, command="PROP", params=["#chan", "-noextmsg"]) + + # Check unset + self.sendLine("chanop", "PROP #chan") + msg = self.getMessage("chanop") + self.assertMessageMatch( + msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST] + ) + self.assertNotIn("noextmsg", msg.params[2:]) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFPROPLIST, + params=["chanop", "#chan", ANYSTR], + ) + self.sendLine("user", "PRIVMSG #chan :hi") + self.assertEqual(self.getMessages("user"), []) + self.assertMessageMatch( + self.getMessage("chanop"), command="PRIVMSG", params=["#chan", "hi"] + ) + + # Set + self.sendLine("chanop", "PROP #chan +noextmsg") + msg = self.getMessage("chanop") + if self.ALLOW_MODE_REPLY and msg.command == "MODE": + self.assertMessageMatch(msg, command="MODE", params=["#chan", "+noextmsg"]) + else: + self.assertMessageMatch(msg, command="PROP", params=["#chan", "+noextmsg"]) + + # Check set again + self.sendLine("chanop", "PROP #chan") + msg = self.getMessage("chanop") + self.assertMessageMatch( + msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST] + ) + self.assertIn("noextmsg", msg.params[2:]) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFPROPLIST, + params=["chanop", "#chan", ANYSTR], + ) + self.sendLine("user", "PRIVMSG #chan :hi") + self.assertMessageMatch( + self.getMessage("user"), + command=ERR_CANNOTSENDTOCHAN, + params=["foo", "#chan", ANYSTR], + ) + self.assertEqual(self.getMessages("chanop"), []) + + @cases.mark_capabilities("draft/named-modes") + @cases.mark_specifications("IRCv3") + def testFlagModeDefaultOff(self): + """Checks flag modes (type 4), using 'inviteonly' as example.""" + self.connectClient("foo", name="user", capabilities=["draft/named-modes"]) + self.connectClient("chanop", name="chanop", capabilities=["draft/named-modes"]) + self.joinChannel("chanop", "#chan") + self.getMessages("chanop") + + # Check unset + self.sendLine("chanop", "PROP #chan") + msg = self.getMessage("chanop") + self.assertMessageMatch( + msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST] + ) + self.assertNotIn("inviteonly", msg.params[2:]) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFPROPLIST, + params=["chanop", "#chan", ANYSTR], + ) + self.sendLine("user", "JOIn #chan") + self.assertMessageMatch( + self.getMessage("user"), command="JOIN", params=["#chan"] + ) + self.sendLine("user", "PART #chan :bye") + self.getMessages("user") + self.getMessages("chanop") + + # Set + self.sendLine("chanop", "PROP #chan +inviteonly") + msg = self.getMessage("chanop") + if self.ALLOW_MODE_REPLY and msg.command == "MODE": + self.assertMessageMatch( + msg, command="MODE", params=["#chan", "+inviteonly"] + ) + else: + self.assertMessageMatch( + msg, command="PROP", params=["#chan", "+inviteonly"] + ) + + # Check set + self.sendLine("chanop", "PROP #chan") + msg = self.getMessage("chanop") + self.assertMessageMatch( + msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST] + ) + self.assertIn("inviteonly", msg.params[2:]) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFPROPLIST, + params=["chanop", "#chan", ANYSTR], + ) + self.sendLine("user", "JOIN #chan") + self.assertMessageMatch(self.getMessage("user"), command=ERR_INVITEONLYCHAN) + + # Unset + self.sendLine("chanop", "PROP #chan -inviteonly") + msg = self.getMessage("chanop") + if self.ALLOW_MODE_REPLY and msg.command == "MODE": + self.assertMessageMatch( + msg, command="MODE", params=["#chan", "-inviteonly"] + ) + else: + self.assertMessageMatch( + msg, command="PROP", params=["#chan", "-inviteonly"] + ) + + # Check unset again + self.sendLine("chanop", "PROP #chan") + msg = self.getMessage("chanop") + self.assertMessageMatch( + msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST] + ) + self.assertNotIn("inviteonly", msg.params[2:]) + self.assertMessageMatch( + self.getMessage("chanop"), + command=RPL_ENDOFPROPLIST, + params=["chanop", "#chan", ANYSTR], + ) + self.sendLine("user", "JOIn #chan") + self.assertMessageMatch( + self.getMessage("user"), command="JOIN", params=["#chan"] + ) + + +class NamedModesTestCase(_NamedModeTestMixin, cases.BaseServerTestCase): + """Normal testing of the named-modes spec.""" + + ALLOW_MODE_REPLY = True + + @cases.mark_capabilities("draft/named-modes") + @cases.mark_specifications("IRCv3") + def testConnectionNumerics(self): + """Tests RPL_CHMODELIST and RPL_UMODELIST.""" + self.addClient() + self.sendLine(1, "CAP LS 302") + self.getCapLs(1) + self.sendLine(1, "USER user user user :user") + self.sendLine(1, "NICK user") + self.sendLine(1, "CAP END") + self.skipToWelcome(1) + msgs = self.getMessages(1) + + seen_chmodes = set() + seen_umodes = set() + + got_last_chmode = False + got_last_umode = False + capturing_re = r"[12345]:(?P(\S+/)?[a-zA-Z0-9-]+)(=[a-zA-Z]+)?" + # fmt: off + chmode_re = r"[12345]:(\S+/)?[a-zA-Z0-9-]+(=[a-zA-Z]+)?" + umode_re = r"[34]:(\S+/)?[a-zA-Z0-9-]+(=[a-zA-Z]+)?" # noqa + # fmt: on + chmode_pat = [ListRemainder(StrRe(chmode_re), min_length=1)] + umode_pat = [ListRemainder(StrRe(umode_re), min_length=1)] + for msg in msgs: + if msg.command == RPL_CHMODELIST: + self.assertFalse( + got_last_chmode, "Got RPL_CHMODELIST after the list ended." + ) + if msg.params[1] == "*": + self.assertMessageMatch( + msg, command=RPL_CHMODELIST, params=["user", "*", *chmode_pat] + ) + else: + self.assertMessageMatch( + msg, command=RPL_CHMODELIST, params=["user", *chmode_pat] + ) + got_last_chmode = True + + for token in msg.params[-1].split(" "): + name = re.match(capturing_re, token).group("name") + self.assertNotIn(name, seen_chmodes, f"Duplicate chmode {name}") + seen_chmodes.add(name) + + elif msg.command == RPL_UMODELIST: + self.assertFalse( + got_last_umode, "Got RPL_UMODELIST after the list ended." + ) + if msg.params[1] == "*": + self.assertMessageMatch( + msg, command=RPL_UMODELIST, params=["user", "*", *umode_pat] + ) + else: + self.assertMessageMatch( + msg, command=RPL_UMODELIST, params=["user", *umode_pat] + ) + got_last_umode = True + + for token in msg.params[-1].split(" "): + name = re.match(capturing_re, token).group("name") + self.assertNotIn(name, seen_umodes, f"Duplicate umode {name}") + seen_umodes.add(name) + + self.assertIn( + "noextmsg", seen_chmodes, "'noextmsg' chmode not supported/advertised" + ) + self.assertIn( + "invisible", seen_umodes, "'invisible' umode not supported/advertised" + ) + + +class OverlyStrictNamedModesTestCase(_NamedModeTestMixin, cases.BaseServerTestCase): + """Stronger tests, that assert the server only sends PROP and never MODE. + Passing these tests is not required to""" + + ALLOW_MODE_REPLY = False diff --git a/irctest/specifications.py b/irctest/specifications.py index 71a5bcf..bd070d1 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -36,6 +36,7 @@ class Capabilities(enum.Enum): MESSAGE_TAGS = "message-tags" MULTILINE = "draft/multiline" MULTI_PREFIX = "multi-prefix" + NAMED_MODES = "draft/named-modes" SERVER_TIME = "server-time" STS = "sts" diff --git a/pytest.ini b/pytest.ini index 221add7..979044c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -27,6 +27,7 @@ markers = message-tags draft/multiline multi-prefix + draft/named-modes server-time sts diff --git a/workflows.yml b/workflows.yml index 4909a0d..b5e4a97 100644 --- a/workflows.yml +++ b/workflows.yml @@ -153,6 +153,7 @@ software: build_script: &inspircd_build_script | cd $GITHUB_WORKSPACE/inspircd/ patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install