diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 7173c8c..74b0520 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -400,6 +400,7 @@ jobs: - test-plexus4 - test-solanum - test-sopel + - test-thelounge - test-unrealircd - test-unrealircd-5 - test-unrealircd-anope @@ -994,6 +995,33 @@ jobs: with: name: pytest-results_sopel_devel path: pytest.xml + test-thelounge: + needs: [] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.7 + uses: actions/setup-python@v4 + with: + python-version: 3.7 + - name: Install dependencies + run: yarn global add https://github.com/thelounge/thelounge.git + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make + thelounge + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v3 + with: + name: pytest-results_thelounge_devel + path: pytest.xml test-unrealircd: needs: - build-unrealircd diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index c3863ac..9341a6c 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -443,6 +443,7 @@ jobs: - test-plexus4 - test-solanum - test-sopel + - test-thelounge - test-unrealircd - test-unrealircd-5 - test-unrealircd-anope @@ -1152,6 +1153,33 @@ jobs: with: name: pytest-results_sopel_stable path: pytest.xml + test-thelounge: + needs: [] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.7 + uses: actions/setup-python@v4 + with: + python-version: 3.7 + - name: Install dependencies + run: yarn global add thelounge@4.4.0 + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make + thelounge + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v3 + with: + name: pytest-results_thelounge_stable + path: pytest.xml test-unrealircd: needs: - build-unrealircd diff --git a/Makefile b/Makefile index f56a249..bd68631 100644 --- a/Makefile +++ b/Makefile @@ -98,6 +98,13 @@ SOPEL_SELECTORS := \ (foo or not foo) \ $(EXTRA_SELECTORS) +# TheLounge can actually pass all the test so there is none to exclude. +# `(foo or not foo)` serves as a `true` value so it doesn't break when +# $(EXTRA_SELECTORS) is non-empty +THELOUNGE_SELECTORS := \ + (foo or not foo) \ + $(EXTRA_SELECTORS) + # 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 # Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs @@ -253,6 +260,11 @@ sopel: --controller=irctest.controllers.sopel \ -k '$(SOPEL_SELECTORS)' +thelounge: + $(PYTEST) $(PYTEST_ARGS) \ + --controller=irctest.controllers.thelounge \ + -k '$(THELOUNGE_SELECTORS)' + unrealircd: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.unrealircd \ diff --git a/irctest/controllers/thelounge.py b/irctest/controllers/thelounge.py new file mode 100644 index 0000000..7992243 --- /dev/null +++ b/irctest/controllers/thelounge.py @@ -0,0 +1,106 @@ +import json +import os +import subprocess +from typing import Optional, Type + +from irctest import authentication, tls +from irctest.basecontrollers import ( + BaseClientController, + DirectoryBasedController, + NotImplementedByController, +) + +TEMPLATE_CONFIG = """ +"use strict"; + +module.exports = {config}; +""" + + +class TheLoungeController(BaseClientController, DirectoryBasedController): + software_name = "TheLounge" + supported_sasl_mechanisms = { + "PLAIN", + "ECDSA-NIST256P-CHALLENGE", + "SCRAM-SHA-256", + "EXTERNAL", + } + supports_sts = True + + def create_config(self) -> None: + super().create_config() + with self.open_file("bot.conf"): + pass + with self.open_file("conf/users.conf"): + pass + + def run( + self, + hostname: str, + port: int, + auth: Optional[authentication.Authentication], + tls_config: Optional[tls.TlsConfig] = None, + ) -> None: + if tls_config is None: + tls_config = tls.TlsConfig(enable=False, trusted_fingerprints=[]) + if tls_config and tls_config.trusted_fingerprints: + raise NotImplementedByController("Trusted fingerprints.") + if auth and any( + mech.to_string().startswith(("SCRAM-", "ECDSA-")) + for mech in auth.mechanisms + ): + raise NotImplementedByController("ecdsa") + if auth and auth.password and len(auth.password) > 300: + # https://github.com/thelounge/thelounge/pull/4480 + # Note that The Lounge truncates on 300 characters, not bytes. + raise NotImplementedByController("Passwords longer than 300 chars") + # Runs a client with the config given as arguments + assert self.proc is None + self.create_config() + if auth: + mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms) + if auth.ecdsa_key: + with self.open_file("ecdsa_key.pem") as fd: + fd.write(auth.ecdsa_key) + else: + mechanisms = "" + + assert self.directory + with self.open_file("config.js") as fd: + fd.write( + TEMPLATE_CONFIG.format( + config=json.dumps( + dict( + public=False, + host=f"unix:{self.directory}/sock", # prevents binding + ) + ) + ) + ) + with self.open_file("users/testuser.json") as fd: + json.dump( + dict( + networks=[ + dict( + name="testnet", + host=hostname, + port=port, + tls=tls_config.enable if tls_config else "False", + sasl=mechanisms.lower(), + saslAccount=auth.username if auth else "", + saslPassword=auth.password if auth else "", + ) + ] + ), + fd, + ) + with self.open_file("users/testuser.json", "r") as fd: + print("config", json.load(fd)["networks"][0]["saslPassword"]) + self.proc = subprocess.Popen( + [os.environ.get("THELOUNGE_BIN", "thelounge"), "start"], + env={**os.environ, "THELOUNGE_HOME": str(self.directory)}, + ) + + +def get_irctest_controller_class() -> Type[TheLoungeController]: + return TheLoungeController diff --git a/workflows.yml b/workflows.yml index 65e27e2..e7b77a2 100644 --- a/workflows.yml +++ b/workflows.yml @@ -379,6 +379,21 @@ software: run: pip install git+https://github.com/sopel-irc/sopel.git devel_release: null + thelounge: + name: TheLounge + separate_build_job: false + install_steps: + stable: + - name: Install dependencies + run: yarn global add thelounge@4.4.0 + release: + - name: Install dependencies + run: yarn global add thelounge + devel: + - name: Install dependencies + run: yarn global add https://github.com/thelounge/thelounge.git + devel_release: null + tests: bahamut: software: [bahamut] @@ -457,3 +472,6 @@ tests: sopel: software: [sopel] + + thelounge: + software: [thelounge]