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)