From 2d2e788275c51ed0d62b72e3caffa061be2b0be4 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 1 Jul 2021 23:10:37 +0200 Subject: [PATCH 1/7] Start adding support for Unreal Not all tests pass yet, Unreal uses the protocol in ways we did not anticipate. --- .github/workflows/unrealircd.yml | 55 +++++++ Makefile | 10 ++ irctest/cases.py | 15 +- irctest/controllers/unrealircd.py | 177 +++++++++++++++++++++++ irctest/server_tests/test_bot_mode.py | 15 +- irctest/server_tests/test_cap.py | 6 +- irctest/server_tests/test_messages.py | 4 + irctest/server_tests/test_regressions.py | 9 +- 8 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/unrealircd.yml create mode 100644 irctest/controllers/unrealircd.py diff --git a/.github/workflows/unrealircd.yml b/.github/workflows/unrealircd.yml new file mode 100644 index 0000000..e1f7801 --- /dev/null +++ b/.github/workflows/unrealircd.yml @@ -0,0 +1,55 @@ +name: irctest with UnrealIRCd + +on: + push: + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v2 + + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cache + $GITHUB_WORKSPACE/unrealircd + key: ${{ runner.os }}-unrealircd + + - name: Install dependencies + run: | + sudo apt-get install atheme-services + python -m pip install --upgrade pip + pip install pytest -r requirements.txt + + - name: Checkout UnrealIRCd + uses: actions/checkout@v2 + with: + repository: unrealircd/unrealircd + ref: unreal52 + path: unrealircd + + - name: Build UnrealIRCd + run: | + cd $GITHUB_WORKSPACE/unrealircd/ + cp $GITHUB_WORKSPACE/unreal_config.settings config.settings + CFLAGS=-O0 ./Config -quick + make -j 4 + echo "\n\n\n\n\n\n" | make pem + make install + + - name: Test with pytest + run: | + PATH=~/.local/unrealircd/bin:$PATH make unreal + + diff --git a/Makefile b/Makefile index 98d011b..2ae1be6 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,16 @@ SOPEL_SELECTORS := \ not testPlainNotAvailable \ $(EXTRA_SELECTORS) +# testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949 +# test_regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948 +# test_messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947 +UNREAL_SELECTORS := \ + not Ergo \ + and not testNoticeNonexistentChannel \ + and not (test_regressions and testTagCap) \ + and not (test_messages and testLineTooLong) \ + $(EXTRA_SELECTORS) + .PHONY: all flakes ergo charybdis all: flakes ergo inspircd limnoria sopel solanum diff --git a/irctest/cases.py b/irctest/cases.py index ea3f06d..9e3fddc 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -517,9 +517,15 @@ class BaseServerTestCase( def getRegistrationMessage(self, client: TClientName) -> Message: """Filter notices, do not send pings.""" - return self.getMessage( - client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE" - ) + while True: + msg = self.getMessage( + client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE" + ) + if msg.command == "PING": + # Hi Unreal + self.sendLine(client, "PONG :" + msg.params[0]) + else: + return msg def sendLine(self, client: TClientName, line: Union[str, bytes]) -> None: return self.clients[client].sendLine(line) @@ -565,6 +571,9 @@ class BaseServerTestCase( result.append(m) if m.command == "001": return result + elif m.command == "PING": + # Hi, Unreal + self.sendLine(client, "PONG :" + m.params[0]) def requestCapabilities( self, diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py new file mode 100644 index 0000000..2f38e6c --- /dev/null +++ b/irctest/controllers/unrealircd.py @@ -0,0 +1,177 @@ +import os +import subprocess +from typing import Optional, Set, Type + +from irctest.basecontrollers import ( + BaseServerController, + DirectoryBasedController, + NotImplementedByController, +) +from irctest.irc_utils.junkdrawer import find_hostname_and_port + +TEMPLATE_CONFIG = """ +include "modules.default.conf"; + +me {{ + name "My.Little.Server"; + info "ExampleNET Server"; + sid "001"; +}} +admin {{ + "Bob Smith"; + "bob"; + "email@example.org"; +}} +class clients {{ + pingfreq 90; + maxclients 1000; + sendq 200k; + recvq 8000; +}} +class servers {{ + pingfreq 60; + connfreq 15; /* try to connect every 15 seconds */ + maxclients 10; /* max servers */ + sendq 20M; +}} +allow {{ + mask *; + class clients; + maxperip 50; + {password_field} +}} +listen {{ + ip {hostname}; + port {port}; +}} +listen {{ + ip {tls_hostname}; + port {tls_port}; + options {{ tls; }} + tls-options {{ + certificate "{pem_path}"; + key "{key_path}"; + }}; +}} + +/* Special SSL/TLS servers-only port for linking */ +listen {{ + ip {services_hostname}; + port {services_port}; + options {{ serversonly; }} +}} + +link services.example.org {{ + incoming {{ + mask localhost; + }} + password "password"; + class servers; +}} +ulines {{ + services.example.org; +}} + +set {{ + kline-address "example@example.org"; + network-name "ExampleNET"; + default-server "irc.example.org"; + help-channel "#Help"; + cloak-keys {{ "aaaA1"; "bbbB2"; "cccC3"; }} + anti-flood {{ + // Prevent throttling, especially test_buffering.py which + // triggers anti-flood with its very long lines + unknown-users {{ + lag-penalty 1; + lag-penalty-bytes 10000; + }} + }} +}} + +tld {{ + mask *; + motd "{empty_file}"; + botmotd "{empty_file}"; + rules "{empty_file}"; +}} +""" + + +class UnrealircdController(BaseServerController, DirectoryBasedController): + software_name = "InspIRCd" + supported_sasl_mechanisms = {"PLAIN"} + supports_sts = False + + def create_config(self) -> None: + super().create_config() + with self.open_file("server.conf"): + pass + + def run( + self, + hostname: str, + port: int, + *, + password: Optional[str], + ssl: bool, + run_services: bool, + valid_metadata_keys: Optional[Set[str]] = None, + invalid_metadata_keys: Optional[Set[str]] = None, + restricted_metadata_keys: Optional[Set[str]] = None, + ) -> None: + if valid_metadata_keys or invalid_metadata_keys: + raise NotImplementedByController( + "Defining valid and invalid METADATA keys." + ) + assert self.proc is None + self.port = port + self.hostname = hostname + self.create_config() + (unused_hostname, unused_port) = find_hostname_and_port() + (services_hostname, services_port) = find_hostname_and_port() + + password_field = 'password "{}";'.format(password) if password else "" + + self.gen_ssl() + if ssl: + (tls_hostname, tls_port) = (hostname, port) + (hostname, port) = (unused_hostname, unused_port) + else: + # Unreal refuses to start without TLS enabled + (tls_hostname, tls_port) = (unused_hostname, unused_port) + + with self.open_file("empty.txt") as fd: + fd.write("\n") + + assert self.directory + with self.open_file("unrealircd.conf") as fd: + fd.write( + TEMPLATE_CONFIG.format( + hostname=hostname, + port=port, + services_hostname=services_hostname, + services_port=services_port, + tls_hostname=tls_hostname, + tls_port=tls_port, + password_field=password_field, + key_path=self.key_path, + pem_path=self.pem_path, + empty_file=os.path.join(self.directory, "empty.txt"), + ) + ) + self.proc = subprocess.Popen( + [ + "unrealircd", + "-F", # BOOT_NOFORK + "-f", + os.path.join(self.directory, "unrealircd.conf"), + ], + stdout=subprocess.DEVNULL, + ) + + if run_services: + assert False + + +def get_irctest_controller_class() -> Type[UnrealircdController]: + return UnrealircdController diff --git a/irctest/server_tests/test_bot_mode.py b/irctest/server_tests/test_bot_mode.py index 23cdd5c..24cdce7 100644 --- a/irctest/server_tests/test_bot_mode.py +++ b/irctest/server_tests/test_bot_mode.py @@ -33,11 +33,16 @@ class BotModeTestCase(cases.BaseServerTestCase): self.sendLine("bot", f"MODE botnick +{self._mode_char}") # Check echoed mode - self.assertMessageMatch( - self.getMessage("bot"), - command="MODE", - params=["botnick", StrRe(r"\+?" + self._mode_char)], - ) + while True: + msg = self.getMessage("bot") + if msg.command != "NOTICE": + # Unreal sends the BOTMOTD here + self.assertMessageMatch( + msg, + command="MODE", + params=["botnick", StrRe(r"\+?" + self._mode_char)], + ) + break def testBotMode(self): self._initBot() diff --git a/irctest/server_tests/test_cap.py b/irctest/server_tests/test_cap.py index 7a7b74e..e177d91 100644 --- a/irctest/server_tests/test_cap.py +++ b/irctest/server_tests/test_cap.py @@ -125,6 +125,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): cap1 = "echo-message" cap2 = "server-time" self.addClient(1) + self.connectClient("sender") self.sendLine(1, "CAP LS 302") m = self.getRegistrationMessage(1) if not ({cap1, cap2} <= set(m.params[2].split())): @@ -146,7 +147,10 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): enabled_caps = set(cap_list.params[2].split()) enabled_caps.discard("cap-notify") # implicitly added by some impls self.assertEqual(enabled_caps, {cap1, cap2}) - self.assertIn("time", cap_list.tags, cap_list) + + self.sendLine(2, "PRIVMSG bar :hi") + m = self.getMessage(1) + self.assertIn("time", m.tags, m) # remove the server-time cap self.sendLine(1, f"CAP REQ :-{cap2}") diff --git a/irctest/server_tests/test_messages.py b/irctest/server_tests/test_messages.py index c8989a7..d4721a4 100644 --- a/irctest/server_tests/test_messages.py +++ b/irctest/server_tests/test_messages.py @@ -74,9 +74,13 @@ class TagsTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("message-tags") def testLineTooLong(self): self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True) + self.connectClient( + "recver", capabilities=["message-tags"], skip_if_cap_nak=True + ) self.joinChannel(1, "#xyz") monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!" self.sendLine(1, monsterMessage) + self.assertEqual(self.getMessages(2), [], "overflowing message was relayed") replies = self.getMessages(1) self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies)) diff --git a/irctest/server_tests/test_regressions.py b/irctest/server_tests/test_regressions.py index 6ccc3f8..8c752cb 100644 --- a/irctest/server_tests/test_regressions.py +++ b/irctest/server_tests/test_regressions.py @@ -109,8 +109,13 @@ class RegressionsTestCase(cases.BaseServerTestCase): self.sendLine(1, "NICK valid") replies = {"NOTICE"} - while replies <= {"NOTICE"}: - replies = set(msg.command for msg in self.getMessages(1, synchronize=False)) + while replies <= {"NOTICE", "PING"}: + msgs = self.getMessages(1, synchronize=False) + for msg in msgs: + if msg.command == "PING": + # Hi Unreal + self.sendLine(1, "PONG :" + msg.params[0]) + replies = set(msg.command for msg in msgs) self.assertNotIn(ERR_ERRONEUSNICKNAME, replies) self.assertIn(RPL_WELCOME, replies) From f83f2a4edf3c4f6416e7fe558777487c824059bf Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 2 Jul 2021 21:41:35 +0200 Subject: [PATCH 2/7] Make all tests pass with Unreal (minus service tests) --- Makefile | 15 +++- irctest/basecontrollers.py | 2 + irctest/controllers/charybdis.py | 1 + irctest/controllers/inspircd.py | 1 + irctest/controllers/unrealircd.py | 21 +++--- irctest/patma.py | 12 ++++ .../server_tests/test_channel_operations.py | 68 ++++++++++++------- .../test_connection_registration.py | 29 +++++++- irctest/server_tests/test_echo_message.py | 3 + .../server_tests/test_labeled_responses.py | 26 +++++-- irctest/server_tests/test_message_tags.py | 4 ++ pytest.ini | 1 + 12 files changed, 139 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 2ae1be6..55c5aba 100644 --- a/Makefile +++ b/Makefile @@ -63,16 +63,22 @@ SOPEL_SELECTORS := \ # testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949 # test_regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948 # test_messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947 -UNREAL_SELECTORS := \ +# testCapRemovalByClient and testNakWhole fail pending https://github.com/unrealircd/unrealircd/pull/148 +# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists tags it relays +UNREALIRCD_SELECTORS := \ not Ergo \ + and not deprecated \ + and not strict \ and not testNoticeNonexistentChannel \ and not (test_regressions and testTagCap) \ and not (test_messages and testLineTooLong) \ + and not (test_cap and (testCapRemovalByClient or testNakWhole)) \ + and not arbitrary_client_tags \ $(EXTRA_SELECTORS) -.PHONY: all flakes ergo charybdis +.PHONY: all flakes charybdis ergo inspircd mammon limnoria sopel solanum unrealircd -all: flakes ergo inspircd limnoria sopel solanum +all: flakes charybdis ergo inspircd mammon limnoria sopel solanum unrealircd flakes: pyflakes3 irctest @@ -97,3 +103,6 @@ solanum: sopel: $(PYTEST) $(PYTEST_ARGS) --controller=irctest.controllers.sopel -k '$(SOPEL_SELECTORS)' + +unrealircd: + $(PYTEST) $(PYTEST_ARGS) --controller=irctest.controllers.unrealircd -k '$(UNREALIRCD_SELECTORS)' diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index adc5371..fa59bbd 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -183,6 +183,8 @@ class BaseServerController(_BaseController): port: int hostname: str services_controller: BaseServicesController + extban_mute_char: Optional[str] = None + """Character used for the 'mute' extban""" def run( self, diff --git a/irctest/controllers/charybdis.py b/irctest/controllers/charybdis.py index 3e84c5b..5094e94 100644 --- a/irctest/controllers/charybdis.py +++ b/irctest/controllers/charybdis.py @@ -73,6 +73,7 @@ class CharybdisController(BaseServerController, DirectoryBasedController): binary_name = "charybdis" supported_sasl_mechanisms = {"PLAIN"} supports_sts = False + extban_mute_char = None def create_config(self) -> None: super().create_config() diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index d28f13f..2186483 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -68,6 +68,7 @@ class InspircdController(BaseServerController, DirectoryBasedController): software_name = "InspIRCd" supported_sasl_mechanisms = {"PLAIN"} supports_sts = False + extban_mute_char = "m" def create_config(self) -> None: super().create_config() diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 2f38e6c..da859cd 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -78,14 +78,17 @@ set {{ default-server "irc.example.org"; help-channel "#Help"; cloak-keys {{ "aaaA1"; "bbbB2"; "cccC3"; }} - anti-flood {{ - // Prevent throttling, especially test_buffering.py which - // triggers anti-flood with its very long lines - unknown-users {{ - lag-penalty 1; - lag-penalty-bytes 10000; - }} + options {{ + identd-check; // Disable it, so it doesn't prefix idents with a tilde + }} + anti-flood {{ + // Prevent throttling, especially test_buffering.py which + // triggers anti-flood with its very long lines + unknown-users {{ + lag-penalty 1; + lag-penalty-bytes 10000; }} + }} }} tld {{ @@ -102,6 +105,8 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): supported_sasl_mechanisms = {"PLAIN"} supports_sts = False + extban_mute_char = "q" + def create_config(self) -> None: super().create_config() with self.open_file("server.conf"): @@ -170,7 +175,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): ) if run_services: - assert False + raise NotImplementedByController("Registration services") def get_irctest_controller_class() -> Type[UnrealircdController]: diff --git a/irctest/patma.py b/irctest/patma.py index 37225ba..19a4780 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -54,6 +54,15 @@ ANYDICT = {RemainingKeys(ANYSTR): AnyOptStr()} `match_dict(got_tags, {"label": "foo", **ANYDICT})`""" +class _AnyListRemainder: + def __repr__(self) -> str: + return "*ANYLIST" + + +ANYLIST = [_AnyListRemainder()] +"""Matches any list remainder""" + + def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bool: if isinstance(expected, AnyOptStr): return True @@ -78,6 +87,9 @@ def match_list( The ANYSTR operator can be used on the 'expected' side as a wildcard, matching any *single* value; and StrRe("") can be used to match regular expressions""" + if expected[-1] is ANYLIST[0]: + expected = expected[0:-1] + got = got[0 : len(expected)] # Ignore remaining if len(got) != len(expected): return False return all( diff --git a/irctest/server_tests/test_channel_operations.py b/irctest/server_tests/test_channel_operations.py index 8415441..c683d5f 100644 --- a/irctest/server_tests/test_channel_operations.py +++ b/irctest/server_tests/test_channel_operations.py @@ -26,7 +26,7 @@ from irctest.numerics import ( RPL_TOPIC, RPL_TOPICTIME, ) -from irctest.patma import ANYSTR, StrRe +from irctest.patma import ANYLIST, ANYSTR, StrRe MODERN_CAPS = [ "server-time", @@ -1296,16 +1296,13 @@ class OpModerated(cases.BaseServerTestCase): class MuteExtban(cases.BaseServerTestCase): """https://defs.ircdocs.horse/defs/isupport.html#extban - These tests assume that if the server advertizes the 'm' extban, - then it supports mute. + It magically guesses what char the IRCd uses for mutes.""" - This is not true of Charybdis, which introduced a conflicting 'm' - exban for matching hostmasks in 2015 - (e2a9fa9cab3720215d8081e940109416e8214a29). - - But Unreal was already using 'm' for muting since 2008 - (f474e7e6dc2d36f96150ebe33b23b4ea76814415) and it is the most popular - definition so we're going with that one.""" + def char(self): + if self.controller.extban_mute_char is None: + raise runner.ExtbanNotSupported("", "mute") + else: + return self.controller.extban_mute_char @cases.mark_specifications("Ergo") def testISupport(self): @@ -1313,7 +1310,7 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport["EXTBAN"] prefix, comma, types = token.partition(",") - self.assertIn("m", types, "Missing 'm' in ISUPPORT EXTBAN") + self.assertIn(self.char, types, f"Missing '{self.char()}' in ISUPPORT EXTBAN") self.assertEqual(prefix, "") self.assertEqual(comma, ",") @@ -1325,15 +1322,15 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport.get("EXTBAN", "") prefix, comma, types = token.partition(",") - if "m" not in types: - raise runner.ExtbanNotSupported("m", "mute") + if self.char() not in types: + raise runner.ExtbanNotSupported(self.char(), "mute") clients = ("chanop", "bar") # Mute "bar" self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:bar!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:bar!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1344,6 +1341,21 @@ class MuteExtban(cases.BaseServerTestCase): for client in clients: self.getMessages(client) + # "bar" sees the MODE too + self.sendLine("bar", "MODE #chan +b") + self.assertMessageMatch( + self.getMessage("bar"), + command="367", + params=[ + "bar", + "#chan", + f"{prefix}{self.char()}:bar!*@*", + "chanop", + *ANYLIST, + ], + ) + self.getMessages("bar") + # "bar" talks: rejected self.sendLine("bar", "PRIVMSG #chan :hi from bar") replies = self.getMessages("bar") @@ -1354,7 +1366,7 @@ class MuteExtban(cases.BaseServerTestCase): # remove mute on "bar" with -b self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan -b m:bar!*@*") + self.sendLine("chanop", f"MODE #chan -b {prefix}{self.char()}:bar!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1378,15 +1390,15 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport.get("EXTBAN", "") prefix, comma, types = token.partition(",") - if "m" not in types: - raise runner.ExtbanNotSupported("m", "mute") + if self.char() not in types: + raise runner.ExtbanNotSupported(self.char(), "mute") clients = ("chanop", "qux") # Mute "qux" self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:qux!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:qux!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1437,17 +1449,17 @@ class MuteExtban(cases.BaseServerTestCase): isupport = self.server_support token = isupport.get("EXTBAN", "") prefix, comma, types = token.partition(",") - if "m" not in types: - raise runner.ExtbanNotSupported("m", "mute") + if self.char() not in types: + raise runner.ExtbanNotSupported(self.char(), "mute") if "e" not in self.server_support["CHANMODES"]: - raise runner.ChannelModeNotSupported("m", "mute") + raise runner.ChannelModeNotSupported(self.char(), "mute") clients = ("chanop", "qux") # Mute "qux" self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:qux!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:qux!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1472,11 +1484,13 @@ class MuteExtban(cases.BaseServerTestCase): self.getMessages(client) # +e grants an exemption to +b - self.sendLine("chanop", "MODE #chan +e m:*!~evan@*") + self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!~evan@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) + self.getMessages("qux") + # so "qux" can now talk self.sendLine("qux", "PRIVMSG #chan :thanks for mute-excepting me") replies = self.getMessages("qux") @@ -1499,10 +1513,14 @@ class MuteExtban(cases.BaseServerTestCase): """ clients = ("chanop", "bar") + isupport = self.server_support + token = isupport.get("EXTBAN", "") + prefix, comma, types = token.partition(",") + self.connectClient("chanop", name="chanop") self.joinChannel("chanop", "#chan") self.getMessages("chanop") - self.sendLine("chanop", "MODE #chan +b m:BAR!*@*") + self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:BAR!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) @@ -1521,7 +1539,7 @@ class MuteExtban(cases.BaseServerTestCase): self.assertEqual(self.getMessages("chanop"), []) # remove mute with -b - self.sendLine("chanop", "MODE #chan -b m:bar!*@*") + self.sendLine("chanop", f"MODE #chan -b {prefix}{self.char()}:bar!*@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) diff --git a/irctest/server_tests/test_connection_registration.py b/irctest/server_tests/test_connection_registration.py index ce2acde..40b8b59 100644 --- a/irctest/server_tests/test_connection_registration.py +++ b/irctest/server_tests/test_connection_registration.py @@ -119,15 +119,38 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase): self.sendLine(2, "NICK foo") self.sendLine(1, "USER username * * :Realname") self.sendLine(2, "USER username * * :Realname") - m1 = self.getRegistrationMessage(1) - m2 = self.getRegistrationMessage(2) + + try: + m1 = self.getRegistrationMessage(1) + except (ConnectionClosed, ConnectionResetError): + # Unreal closes the connection, see + # https://bugs.unrealircd.org/view.php?id=5950 + command1 = None + else: + command1 = m1.command + + try: + m2 = self.getRegistrationMessage(2) + except (ConnectionClosed, ConnectionResetError): + # ditto + command2 = None + else: + command2 = m2.command + self.assertNotEqual( - (m1.command, m2.command), + (command1, command2), ("001", "001"), "Two concurrently registering requesting the same nickname " "both got 001.", ) + self.assertIn( + "001", + (command1, command2), + "Two concurrently registering requesting the same nickname " + "neither got 001.", + ) + @cases.mark_specifications("IRCv3") def testIrc301CapLs(self): """ diff --git a/irctest/server_tests/test_echo_message.py b/irctest/server_tests/test_echo_message.py index d43aafa..4226697 100644 --- a/irctest/server_tests/test_echo_message.py +++ b/irctest/server_tests/test_echo_message.py @@ -2,6 +2,8 @@ """ +import pytest + from irctest import cases from irctest.basecontrollers import NotImplementedByController from irctest.irc_utils.junkdrawer import random_name @@ -97,6 +99,7 @@ def _testEchoMessage(command, solo, server_time): class EchoMessageTestCase(cases.BaseServerTestCase): + @pytest.mark.arbitrary_client_tags @cases.mark_capabilities( "batch", "labeled-response", "echo-message", "message-tags" ) diff --git a/irctest/server_tests/test_labeled_responses.py b/irctest/server_tests/test_labeled_responses.py index ad4274c..239abf5 100644 --- a/irctest/server_tests/test_labeled_responses.py +++ b/irctest/server_tests/test_labeled_responses.py @@ -282,7 +282,14 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper ) self.getMessages(2) - self.sendLine(1, "@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG bar") + # Need to get a valid msgid because Unreal validates them + self.sendLine(1, "PRIVMSG bar :hi") + msgid = self.getMessage(1).tags["msgid"] + assert msgid == self.getMessage(2).tags["msgid"] + + self.sendLine( + 1, f"@label=12345;+draft/reply={msgid};+draft/react=l😃l TAGMSG bar" + ) m = self.getMessage(1) m2 = self.getMessage(2) @@ -290,7 +297,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch( m2, command="TAGMSG", - tags={"+draft/reply": "123", "+draft/react": "l😃l", **ANYDICT}, + tags={"+draft/reply": msgid, "+draft/react": "l😃l", **ANYDICT}, ) self.assertNotIn( "label", @@ -308,7 +315,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper command="TAGMSG", tags={ "label": "12345", - "+draft/reply": "123", + "+draft/reply": msgid, "+draft/react": "l😃l", **ANYDICT, }, @@ -338,7 +345,14 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.getMessages(2) self.getMessages(1) - self.sendLine(1, "@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG #test") + # Need to get a valid msgid because Unreal validates them + self.sendLine(1, "PRIVMSG #test :hi") + msgid = self.getMessage(1).tags["msgid"] + assert msgid == self.getMessage(2).tags["msgid"] + + self.sendLine( + 1, f"@label=12345;+draft/reply={msgid};+draft/react=l😃l TAGMSG #test" + ) ms = self.getMessage(1) mt = self.getMessage(2) @@ -361,7 +375,9 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper # ensure sender correctly receives msg self.assertMessageMatch( - ms, command="TAGMSG", tags={"label": "12345", **ANYDICT} + ms, + command="TAGMSG", + tags={"label": "12345", "+draft/reply": msgid, **ANYDICT}, ) @cases.mark_capabilities( diff --git a/irctest/server_tests/test_message_tags.py b/irctest/server_tests/test_message_tags.py index ce190f7..a0f4745 100644 --- a/irctest/server_tests/test_message_tags.py +++ b/irctest/server_tests/test_message_tags.py @@ -2,6 +2,8 @@ https://ircv3.net/specs/extensions/message-tags.html """ +import pytest + from irctest import cases from irctest.irc_utils.message_parser import parse_message from irctest.numerics import ERR_INPUTTOOLONG @@ -9,6 +11,7 @@ from irctest.patma import ANYDICT, ANYSTR, StrRe class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): + @pytest.mark.arbitrary_client_tags @cases.mark_capabilities("message-tags") def testBasic(self): def getAllMessages(): @@ -107,6 +110,7 @@ class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): self.assertNotIn("cat", msg.tags) self.assertEqual(alice_msg.tags["msgid"], bob_msg.tags["msgid"]) + @pytest.mark.arbitrary_client_tags @cases.mark_capabilities("message-tags") @cases.mark_specifications("ircdocs") def testLengthLimits(self): diff --git a/pytest.ini b/pytest.ini index 9eb742d..eadf6f4 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,6 +12,7 @@ markers = strict deprecated services + arbitrary_client_tags # capabilities account-tag From a1040a4553d8b378976d5337633cbe4448ccf3d6 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 2 Jul 2021 21:48:12 +0200 Subject: [PATCH 3/7] Minor bug fixes --- .../server_tests/test_channel_operations.py | 3 ++- unreal_config.settings | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 unreal_config.settings diff --git a/irctest/server_tests/test_channel_operations.py b/irctest/server_tests/test_channel_operations.py index c683d5f..e0b3e54 100644 --- a/irctest/server_tests/test_channel_operations.py +++ b/irctest/server_tests/test_channel_operations.py @@ -1513,11 +1513,12 @@ class MuteExtban(cases.BaseServerTestCase): """ clients = ("chanop", "bar") + self.connectClient("chanop", name="chanop") + isupport = self.server_support token = isupport.get("EXTBAN", "") prefix, comma, types = token.partition(",") - self.connectClient("chanop", name="chanop") self.joinChannel("chanop", "#chan") self.getMessages("chanop") self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:BAR!*@*") diff --git a/unreal_config.settings b/unreal_config.settings new file mode 100644 index 0000000..e4d15b1 --- /dev/null +++ b/unreal_config.settings @@ -0,0 +1,24 @@ +BASEPATH="$HOME/.local/unrealircd" +BINDIR="$HOME/.local/unrealircd/bin" +DATADIR="$HOME/.local/unrealircd/data" +CONFDIR="$HOME/.local/unrealircd/conf" +MODULESDIR="$HOME/.local/unrealircd/modules" +LOGDIR="$HOME/.local/unrealircd/logs" +CACHEDIR="$HOME/.local/unrealircd/cache" +DOCDIR="$HOME/.local/unrealircd/doc" +TMPDIR="$HOME/.local/unrealircd/tmp" +PRIVATELIBDIR="$HOME/.local/unrealircd/lib" +PREFIXAQ="1" +MAXCONNECTIONS_REQUEST="auto" +NICKNAMEHISTORYLENGTH="2000" +DEFPERM="0600" +SSLDIR="" +REMOTEINC="" +CURLDIR="" +SHOWLISTMODES="1" +NOOPEROVERRIDE="" +OPEROVERRIDEVERIFY="" +GENCERTIFICATE="1" +EXTRAPARA="" +ADVANCED="" + From 5674bb030ab0b45a679b55ab8f0caf0e2a6c7ff3 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 2 Jul 2021 21:53:30 +0200 Subject: [PATCH 4/7] uh, openssl doesn't like my echo when running on GH Actions --- .github/workflows/unrealircd.yml | 3 +-- unreal_config.settings => unreal/config.settings | 0 unreal/server.cert.pem | 14 ++++++++++++++ unreal/server.key.pem | 9 +++++++++ unreal/server.req.pem | 9 +++++++++ 5 files changed, 33 insertions(+), 2 deletions(-) rename unreal_config.settings => unreal/config.settings (100%) create mode 100644 unreal/server.cert.pem create mode 100644 unreal/server.key.pem create mode 100644 unreal/server.req.pem diff --git a/.github/workflows/unrealircd.yml b/.github/workflows/unrealircd.yml index e1f7801..adf8690 100644 --- a/.github/workflows/unrealircd.yml +++ b/.github/workflows/unrealircd.yml @@ -42,10 +42,9 @@ jobs: - name: Build UnrealIRCd run: | cd $GITHUB_WORKSPACE/unrealircd/ - cp $GITHUB_WORKSPACE/unreal_config.settings config.settings + cp $GITHUB_WORKSPACE/unreal/* . CFLAGS=-O0 ./Config -quick make -j 4 - echo "\n\n\n\n\n\n" | make pem make install - name: Test with pytest diff --git a/unreal_config.settings b/unreal/config.settings similarity index 100% rename from unreal_config.settings rename to unreal/config.settings diff --git a/unreal/server.cert.pem b/unreal/server.cert.pem new file mode 100644 index 0000000..28a521d --- /dev/null +++ b/unreal/server.cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGDCCAZ6gAwIBAgIUeHAOQnvT7N9kCmUuIklelkzz8SUwCgYIKoZIzj0EAwIw +QzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRIwEAYDVQQKDAlJUkMg +Z2Vla3MxDTALBgNVBAsMBElSQ2QwHhcNMjEwNzAyMTk1MTM5WhcNMzEwNjMwMTk1 +MTM5WjBDMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxEjAQBgNVBAoM +CUlSQyBnZWVrczENMAsGA1UECwwESVJDZDB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BHA6iqLQgkS42xHg/dEPq9dKjlLi0HWvCM7nOCXAyFy1DjrmbFoSCQBCUbISsk/C +Txru3YIfXe6jSCS8UTb15m70mrmmiUr/umxiqjAOiso051hCrzxVmjTpEAqMSnrc +zKNTMFEwHQYDVR0OBBYEFFNHqsBNxDNhVxfAgdv6/y4Xd6/ZMB8GA1UdIwQYMBaA +FFNHqsBNxDNhVxfAgdv6/y4Xd6/ZMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E +AwIDaAAwZQIwAo29xUEAzqOMgPAWtMifHFLuPQPuWcNGbaI5S4W81NO8uIcNv/kM +mFocuITr76p0AjEApzGjc5wM+KydwoVTP+fg1aGQA13Ba2nCzN3R5XwR/USCigjv +na1QtWAKjpvR/rsp +-----END CERTIFICATE----- diff --git a/unreal/server.key.pem b/unreal/server.key.pem new file mode 100644 index 0000000..2f1cc45 --- /dev/null +++ b/unreal/server.key.pem @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCWkDHktJiTqC7im+Ni37fbXxtMBqIKPwkAItpKMeuh28QrXWwNE1a5 +wSa38C1nd8igBwYFK4EEACKhZANiAARwOoqi0IJEuNsR4P3RD6vXSo5S4tB1rwjO +5zglwMhctQ465mxaEgkAQlGyErJPwk8a7t2CH13uo0gkvFE29eZu9Jq5polK/7ps +YqowDorKNOdYQq88VZo06RAKjEp63Mw= +-----END EC PRIVATE KEY----- diff --git a/unreal/server.req.pem b/unreal/server.req.pem new file mode 100644 index 0000000..49a4f8f --- /dev/null +++ b/unreal/server.req.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBOjCBwgIBADBDMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxEjAQ +BgNVBAoMCUlSQyBnZWVrczENMAsGA1UECwwESVJDZDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABHA6iqLQgkS42xHg/dEPq9dKjlLi0HWvCM7nOCXAyFy1DjrmbFoSCQBC +UbISsk/CTxru3YIfXe6jSCS8UTb15m70mrmmiUr/umxiqjAOiso051hCrzxVmjTp +EAqMSnrczKAAMAoGCCqGSM49BAMCA2cAMGQCMEL5ezlauGUaxh+pXt897ffmzqci +fqYm3FgVW5x6EdtCxtcwwAwnR84LKcd/YRKOygIwNmZiRVKeSeC7Ess1PxuzT1Mu +Cw3bBqkE5LmO1hu/+0lK+QoFPEeLDrygIh+SDdGH +-----END CERTIFICATE REQUEST----- From c4d19d44e8b024b40a04ef80d0599b55f8b9a3b1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 3 Jul 2021 09:31:51 +0200 Subject: [PATCH 5/7] test_labeled_responses: Actually check 'label' tags aren't relayed The existing assertion's comment said it checked the label wasn't relayed, but the code actually let any tag through. --- irctest/patma.py | 11 +++++++++ irctest/self_tests/test_cases.py | 23 ++++++++++++++++++- .../server_tests/test_labeled_responses.py | 13 +++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/irctest/patma.py b/irctest/patma.py index 19a4780..fd9db2b 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -35,6 +35,14 @@ class StrRe(Operator): return f"StrRe(r'{self.regexp}')" +@dataclasses.dataclass(frozen=True) +class NotStrRe(Operator): + regexp: str + + def __repr__(self) -> str: + return f"NotStrRe(r'{self.regexp}')" + + @dataclasses.dataclass(frozen=True) class RemainingKeys(Operator): """Used in a dict pattern to match all remaining keys. @@ -71,6 +79,9 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo elif isinstance(expected, StrRe): if got is None or not re.match(expected.regexp, got): return False + elif isinstance(expected, NotStrRe): + if got is None or re.match(expected.regexp, got): + return False elif isinstance(expected, Operator): raise NotImplementedError(f"Unsupported operator: {expected}") elif got != expected: diff --git a/irctest/self_tests/test_cases.py b/irctest/self_tests/test_cases.py index 9bd479e..e31ea51 100644 --- a/irctest/self_tests/test_cases.py +++ b/irctest/self_tests/test_cases.py @@ -4,7 +4,7 @@ import pytest from irctest import cases from irctest.irc_utils.message_parser import parse_message -from irctest.patma import ANYDICT, ANYSTR, StrRe +from irctest.patma import ANYDICT, ANYSTR, AnyOptStr, NotStrRe, RemainingKeys, StrRe # fmt: off MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ @@ -131,6 +131,27 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ ":foo!baz@qux PRIVMSG #chan hello", ] ), + ( + # the specification: + dict( + tags={"tag1": "bar", RemainingKeys(NotStrRe("tag2")): AnyOptStr()}, + command="PRIVMSG", + params=["#chan", "hello"], + ), + # matches: + [ + "@tag1=bar PRIVMSG #chan :hello", + "@tag1=bar :foo!baz@qux PRIVMSG #chan :hello", + "@tag1=bar;tag3= PRIVMSG #chan :hello", + ], + # and does not match: + [ + "PRIVMG #chan :hello", + "@tag1=value1 PRIVMSG #chan :hello", + "@tag1=bar;tag2= PRIVMSG #chan :hello", + "@tag1=bar;tag2=baz PRIVMSG #chan :hello", + ] + ), ] # fmt: on diff --git a/irctest/server_tests/test_labeled_responses.py b/irctest/server_tests/test_labeled_responses.py index 239abf5..4ad03ab 100644 --- a/irctest/server_tests/test_labeled_responses.py +++ b/irctest/server_tests/test_labeled_responses.py @@ -8,7 +8,7 @@ so there may be many false positives. import re from irctest import cases -from irctest.patma import ANYDICT, StrRe +from irctest.patma import ANYDICT, AnyOptStr, NotStrRe, RemainingKeys, StrRe class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): @@ -297,7 +297,11 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch( m2, command="TAGMSG", - tags={"+draft/reply": msgid, "+draft/react": "l😃l", **ANYDICT}, + tags={ + "+draft/reply": msgid, + "+draft/react": "l😃l", + RemainingKeys(NotStrRe("label")): AnyOptStr(), + }, ) self.assertNotIn( "label", @@ -360,6 +364,11 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch( mt, command="TAGMSG", + tags={ + "+draft/reply": msgid, + "+draft/react": "l😃l", + RemainingKeys(NotStrRe("label")): AnyOptStr(), + }, fail_msg="No TAGMSG received by the target after sending one out", ) self.assertNotIn( From 4be59a77edd17da58bf860d82f261015b1ee6c44 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 3 Jul 2021 09:39:51 +0200 Subject: [PATCH 6/7] .github/workflows/unrealircd.yml: Actually run the tests --- .github/workflows/unrealircd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unrealircd.yml b/.github/workflows/unrealircd.yml index adf8690..0edbd0c 100644 --- a/.github/workflows/unrealircd.yml +++ b/.github/workflows/unrealircd.yml @@ -49,6 +49,6 @@ jobs: - name: Test with pytest run: | - PATH=~/.local/unrealircd/bin:$PATH make unreal + PATH=~/.local/unrealircd/bin:$PATH make unrealircd From 83152bdc24b2d093d2b1ae06c42348b4149dbe20 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 3 Jul 2021 09:40:49 +0200 Subject: [PATCH 7/7] unreal: deselect tests depending on +draft/react Unreal won't support them 1st-party: https://github.com/unrealircd/unrealircd/pull/149 --- Makefile | 4 +++- irctest/server_tests/test_labeled_responses.py | 7 +++++++ pytest.ini | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 55c5aba..85b29ce 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,8 @@ SOPEL_SELECTORS := \ # test_regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948 # test_messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947 # testCapRemovalByClient and testNakWhole fail pending https://github.com/unrealircd/unrealircd/pull/148 -# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists tags it relays +# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays +# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149 UNREALIRCD_SELECTORS := \ not Ergo \ and not deprecated \ @@ -74,6 +75,7 @@ UNREALIRCD_SELECTORS := \ and not (test_messages and testLineTooLong) \ and not (test_cap and (testCapRemovalByClient or testNakWhole)) \ and not arbitrary_client_tags \ + and not react_tag \ $(EXTRA_SELECTORS) .PHONY: all flakes charybdis ergo inspircd mammon limnoria sopel solanum unrealircd diff --git a/irctest/server_tests/test_labeled_responses.py b/irctest/server_tests/test_labeled_responses.py index 4ad03ab..0cc1bf5 100644 --- a/irctest/server_tests/test_labeled_responses.py +++ b/irctest/server_tests/test_labeled_responses.py @@ -7,6 +7,8 @@ so there may be many false positives. import re +import pytest + from irctest import cases from irctest.patma import ANYDICT, AnyOptStr, NotStrRe, RemainingKeys, StrRe @@ -89,6 +91,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch(m, command="PRIVMSG", tags={"label": "12345"}) + @pytest.mark.react_tag @cases.mark_capabilities("echo-message", "batch", "labeled-response") def testLabeledPrivmsgResponsesToChannel(self): self.connectClient( @@ -190,6 +193,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper self.assertMessageMatch(m, command="NOTICE", tags={"label": "12345"}) + @pytest.mark.react_tag @cases.mark_capabilities("echo-message", "batch", "labeled-response") def testLabeledNoticeResponsesToChannel(self): self.connectClient( @@ -265,6 +269,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper ).format(number_of_labels), ) + @pytest.mark.react_tag @cases.mark_capabilities( "echo-message", "batch", "labeled-response", "message-tags" ) @@ -325,6 +330,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper }, ) + @pytest.mark.react_tag @cases.mark_capabilities( "echo-message", "batch", "labeled-response", "message-tags" ) @@ -389,6 +395,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper tags={"label": "12345", "+draft/reply": msgid, **ANYDICT}, ) + @pytest.mark.react_tag @cases.mark_capabilities( "echo-message", "batch", "labeled-response", "message-tags" ) diff --git a/pytest.ini b/pytest.ini index eadf6f4..21bec0b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -13,6 +13,7 @@ markers = deprecated services arbitrary_client_tags + react_tag # capabilities account-tag