From d7009d81ac81dd8f100fed34f42da58bc1a4e357 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Sat, 4 Apr 2026 13:06:12 +0200 Subject: [PATCH 1/5] Absorb matrix-is-tester as uv workspace member MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy matrix-is-tester into integration-tests/ as a workspace member. Merge matrix_is_test (launcher + templates) into the same package. Fix launcher import path and use 'uv run' to start Sydent subprocess. No code modernisation yet — files are copied as-is from the external repo. --- .github/workflows/pipeline.yml | 3 +- Dockerfile | 1 + integration-tests/README.md | 20 ++ .../matrix_is_tester}/__init__.py | 0 .../matrix_is_tester/base_api_test.py | 160 +++++++++++++ integration-tests/matrix_is_tester/fakehs.pem | 45 ++++ integration-tests/matrix_is_tester/fakehs.py | 137 +++++++++++ integration-tests/matrix_is_tester/is_api.py | 220 ++++++++++++++++++ .../matrix_is_tester/launch_is.py | 45 ++++ .../matrix_is_tester}/launcher.py | 24 +- .../matrix_is_tester/mailsink.py | 72 ++++++ .../res/is-test/invite_template.eml | 0 .../res/is-test/invite_template.eml.j2 | 0 .../res/is-test/verification_template.eml | 0 .../res/is-test/verification_template.eml.j2 | 0 .../res/is-test/verify_response_template.html | 0 .../matrix_is_tester}/terms.yaml | 0 .../matrix_is_tester/test_account.py | 49 ++++ .../matrix_is_tester/test_bind_denied.py | 51 ++++ .../matrix_is_tester/test_logout.py | 51 ++++ .../matrix_is_tester/test_terms.py | 159 +++++++++++++ integration-tests/matrix_is_tester/test_v1.py | 81 +++++++ integration-tests/matrix_is_tester/test_v2.py | 65 ++++++ .../matrix_is_tester/test_versions.py | 47 ++++ integration-tests/pyproject.toml | 20 ++ pyproject.toml | 9 +- scripts/casefold_db.py | 1 + tests/test_auth.py | 1 + tests/test_blacklisting.py | 1 + tests/test_casefold_migration.py | 1 + tests/test_email.py | 1 + tests/test_invites.py | 1 + tests/test_jinja_templates.py | 1 + tests/test_msisdn.py | 1 + tests/test_replication.py | 1 + tests/test_store_invite.py | 1 + uv.lock | 19 +- 37 files changed, 1268 insertions(+), 20 deletions(-) create mode 100644 integration-tests/README.md rename {matrix_is_test => integration-tests/matrix_is_tester}/__init__.py (100%) create mode 100644 integration-tests/matrix_is_tester/base_api_test.py create mode 100644 integration-tests/matrix_is_tester/fakehs.pem create mode 100644 integration-tests/matrix_is_tester/fakehs.py create mode 100644 integration-tests/matrix_is_tester/is_api.py create mode 100644 integration-tests/matrix_is_tester/launch_is.py rename {matrix_is_test => integration-tests/matrix_is_tester}/launcher.py (84%) create mode 100644 integration-tests/matrix_is_tester/mailsink.py rename {matrix_is_test => integration-tests/matrix_is_tester}/res/is-test/invite_template.eml (100%) rename {matrix_is_test => integration-tests/matrix_is_tester}/res/is-test/invite_template.eml.j2 (100%) rename {matrix_is_test => integration-tests/matrix_is_tester}/res/is-test/verification_template.eml (100%) rename {matrix_is_test => integration-tests/matrix_is_tester}/res/is-test/verification_template.eml.j2 (100%) rename {matrix_is_test => integration-tests/matrix_is_tester}/res/is-test/verify_response_template.html (100%) rename {matrix_is_test => integration-tests/matrix_is_tester}/terms.yaml (100%) create mode 100755 integration-tests/matrix_is_tester/test_account.py create mode 100644 integration-tests/matrix_is_tester/test_bind_denied.py create mode 100755 integration-tests/matrix_is_tester/test_logout.py create mode 100755 integration-tests/matrix_is_tester/test_terms.py create mode 100755 integration-tests/matrix_is_tester/test_v1.py create mode 100755 integration-tests/matrix_is_tester/test_v2.py create mode 100755 integration-tests/matrix_is_tester/test_versions.py create mode 100644 integration-tests/pyproject.toml diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index a5d05139..468fbf0a 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -77,13 +77,14 @@ jobs: strategy: matrix: python-version: ['3.10', '3.13'] + test-dir: ['tests', 'matrix_is_tester'] steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} - - run: uv run trial tests + - run: uv run trial ${{ matrix.test-dir }} # a job which runs once all the other jobs are complete, thus allowing PRs to # be merged. diff --git a/Dockerfile b/Dockerfile index 09f02af2..5080a440 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + --mount=type=bind,source=integration-tests/pyproject.toml,target=integration-tests/pyproject.toml \ uv sync --locked --no-install-project --no-dev --extra sentry --extra prometheus # Step 2: Copy source and install the project diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 00000000..7e65625d --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,20 @@ +matrix-is-tester +================ + +matrix-is-tester is an integration testing system for Matrix Identity servers, similar +to sytest (although using python rather than perl). + +To launch the IS server, it attempts to import +`matrix_is_test.launcher.MatrixIsTestLauncher`. This must be provided separately +for the specific ID server implementation to be tested. See sydent's implementation +for an example of how this works. + +Sydent supplies its matrix-is-tester launcher in the project directory, so to launch +matrix_is_tester on sydent you would run: + +``` +PYTHONPATH="/path/to/sydent" trial matrix_is_tester +``` + +...which puts the launcher on the PYTHONPATH and invokes trial on matrix_is_tester (which +is assumed to already be on sys.path). diff --git a/matrix_is_test/__init__.py b/integration-tests/matrix_is_tester/__init__.py similarity index 100% rename from matrix_is_test/__init__.py rename to integration-tests/matrix_is_tester/__init__.py diff --git a/integration-tests/matrix_is_tester/base_api_test.py b/integration-tests/matrix_is_tester/base_api_test.py new file mode 100644 index 00000000..f32b1767 --- /dev/null +++ b/integration-tests/matrix_is_tester/base_api_test.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +# These are standard python unit tests, but are generally intended +# to be run with trial. Trial doesn't capture logging nicely if you +# use python 'logging': it only works if you use Twisted's own. +from twisted.python import log + +from matrix_is_tester.is_api import IsApi +from matrix_is_tester.launch_is import get_or_launch_is +from matrix_is_tester.mailsink import get_shared_mailsink + + +class BaseApiTest(object): + """ + Not a test case itself, but can be subclassed to test APIs common + between versions. + """ + + def setUp(self): + self.baseUrl = get_or_launch_is() + + self.mailSink = get_shared_mailsink() + + self.api = IsApi(self.baseUrl, self.API_VERSION, self.mailSink) + + def test_ping(self): + body = self.api.ping() + self.assertEquals(body, {}) + + def test_request_email_code(self): + body = self.api.request_email_code("fakeemail1@nowhere.test", "sekrit", 1) + log.msg("Got response %r" % (body,)) + self.assertIn("sid", body) + self.mailSink.get_mail() + + def test_reject_invalid_email(self): + body = self.api.request_email_code( + "fakeemail1@nowhere.test@elsewhere.test", "sekrit", 1 + ) + self.assertEquals(body["errcode"], "M_INVALID_EMAIL") + + def test_submit_email_code(self): + self.api.request_and_submit_email_code("fakeemail2@nowhere.test") + + def test_submit_email_code_get(self): + req_response = self.api.request_email_code( + "steve@nowhere.test", "verysekrit", 1 + ) + sid = req_response["sid"] + + token = self.api.get_token_from_mail() + + body = self.api.submit_email_token_via_get(sid, "verysekrit", token) + self.assertEquals(body, b"matrix_is_tester:email_submit_get_response\n") + + body = self.api.get_validated_threepid(sid, "verysekrit") + + self.assertEquals(body["medium"], "email") + self.assertEquals(body["address"], "steve@nowhere.test") + + def test_unverified_bind(self): + req_code_body = self.api.request_email_code( + "fakeemail5@nowhere.test", "sekrit", 1 + ) + # get the mail so we don't leave it in the queue + self.mailSink.get_mail() + body = self.api.bind_email( + req_code_body["sid"], "sekrit", "@commonapitests:127.0.0.1:4490" + ) + self.assertEquals(body["errcode"], "M_SESSION_NOT_VALIDATED") + + def test_get_validated_threepid(self): + params = self.api.request_and_submit_email_code("fakeemail4@nowhere.test") + + body = self.api.get_validated_threepid(params["sid"], params["client_secret"]) + + self.assertEquals(body["medium"], "email") + self.assertEquals(body["address"], "fakeemail4@nowhere.test") + + def test_get_validated_threepid_not_validated(self): + req_code_body = self.api.request_email_code( + "fakeemail5@nowhere.test", "sekrit", 1 + ) + # get the mail, otherwise the next test will get it + # instead of the one it was expecting + self.mailSink.get_mail() + + get_val_body = self.api.get_validated_threepid(req_code_body["sid"], "sekrit") + self.assertEquals(get_val_body["errcode"], "M_SESSION_NOT_VALIDATED") + + def test_store_invite(self): + body = self.api.store_invite( + { + "medium": "email", + "address": "ian@fake.test", + "room_id": "$aroom:fake.test", + "sender": "@commonapitests:127.0.0.1:4490", + "room_alias": "#alias:fake.test", + "room_avatar_url": "mxc://fake.test/roomavatar", + "room_name": "my excellent room", + "sender_display_name": "Ian Sender", + "sender_avatar_url": "mxc://fake.test/iansavatar", + } + ) + self.assertGreater(len(body["token"]), 0) + # must be redacted + self.assertNotEqual(body["display_name"], "ian@fake.test") + self.assertGreater(len(body["public_keys"]), 0) + + for k in body["public_keys"]: + is_valid_body = self.api.pubkey_is_valid( + k["key_validity_url"], k["public_key"] + ) + self.assertTrue(is_valid_body["valid"]) + + mail = self.mailSink.get_mail() + log.msg("Got email (invite): %r" % (mail,)) + mail_object = json.loads(mail["data"]) + self.assertEquals(mail_object["token"], body["token"]) + self.assertEquals(mail_object["room_alias"], "#alias:fake.test") + self.assertEquals(mail_object["room_avatar_url"], "mxc://fake.test/roomavatar") + self.assertEquals(mail_object["room_name"], "my excellent room") + self.assertEquals(mail_object["sender_display_name"], "Ian Sender") + self.assertEquals( + mail_object["sender_avatar_url"], "mxc://fake.test/iansavatar" + ) + + def test_store_invite_bound_threepid(self): + params = self.api.request_and_submit_email_code("already_here@fake.test") + self.api.bind_email( + params["sid"], params["client_secret"], "@some_mxid:fake.test" + ) + + body = self.api.store_invite( + { + "medium": "email", + "address": "already_here@fake.test", + "room_id": "$aroom:fake.test", + "sender": "@commonapitests:127.0.0.1:4490", + } + ) + self.assertEquals(body["errcode"], "M_THREEPID_IN_USE") diff --git a/integration-tests/matrix_is_tester/fakehs.pem b/integration-tests/matrix_is_tester/fakehs.pem new file mode 100644 index 00000000..7a415ce7 --- /dev/null +++ b/integration-tests/matrix_is_tester/fakehs.pem @@ -0,0 +1,45 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDP2WZtaCEt5WKM +NdQ/38/mb0Se58SuR6dyxkElC8aezrLpGPRBIjzyg7WKRiuu4mIvzEv5oqr/GEQO +8zsNKsrtyLUuayT5eJPGVEsCDwi+1GguuxjG75a/zQBMgBkRAD4bij/EBAPsPJBs +iENjphMYOylU1lmwvx2mN+ehZOeCP9sGRmUYgZRi5XYl5TtCXwpO4bi8iDzGw7a2 +yhLqHXAWugYKHLnDauq4nPbBIdplUV6UqZ3i4KKPjta/EFIReURh3ZqBdLm94uDV +7wZM0WyCbccW0f0d8INjoLJFD7PfQJrjtvPtnrh+9tRL+FyPHAaSZHSYU+Kvm5CW +wYZXMrrnAgMBAAECggEAak0305DEF2MP6cHGEfz3qVUS9Wp37uJ6w3qd6sKBDMuO +OSUoFv/Zx/aQrG2C/eiOav/Dg6MsbVcNx8+iTfOq4b4a2+i0elquyWpnCmCCCoc7 +2VqbK3Nx2BqSoo2JRGapXRBx2GBtWS8IdlmijZ5seaIYW2ldacX09gP1lVe0B6qQ +1rA4Hvl3847cY+Aodu1fL/qPvXYvhrxzEsLVzKCqEJpQWlPQlq8d3RrPa4JLT4P7 +IJ9pKodcfc1GMdVI6PeBeAESSmwUBjlD0AmrR9BY9mai0gXs8d2I+ZAmVHqg2A4H +Q3r6gGOI3abpA4WQst1FdnBFniMnC22G54YaacfZkQKBgQDui6P69SlpN0Jm6lKF +eUuFSuYG0FczSDEO+hu4kCqkUc4QWfufBcGN0t9oOgZJeTgpQrN7AVljKPukcOcY +1/9td2y10KQki+oHi5dFqKiHTcqR29WM2Xz0fTw2gVYkkS1DFdu2RBJGBR33esnX +ew+CN9amwliVj2XURduAEexJ3QKBgQDfDsRVxFHGxdnWX0R3j4EuPbncPDhFI0HC +H6aidIoQDkVrl/4xd4EIHg48vbBTo4+mDD2Urtcw8Pi/Sd9kWFBRVhzsHk5Ls/x4 +AcIW74/UhOmmDJ6gBRH5AnXGp6ga7pUgtaiKww7nkWNf5E1UHhHcY08Jf5QH3OMQ +EBe7Ja8FkwKBgQDlo4QeqThOU6YW0OjUGSp8jNfYI2Rut8aSdm+NQyvpt965mwZB +1ha4YxIykflPbeSEw/NoLKpSbTei3BV8syLvzJHYjZwWmqKW1OixZGWoq1ihBZIU +36IM8yquBeBZn3CFLluuoOU+htqMTaZVS+BoKTz4mAsTH1KWARIHvjlL+QKBgAn7 +ijPgblx7/EzIxLKpHHnqT0gY9de6RTYf3oBEwO0JBnhTPBAQrhij57U2NA76MfKX +d6YQ0Raioi9Fahb+kNGjDfZPQOfIbVMdmQcXv5MeQ6qnw+2bbHt9bbHvTOmvpcLp +/ln/cspQSmc/O2q5UclQNHhTWlejvhG49qbsf9G7AoGAIZNXK7/+YyISVunI17DN +BDurmFabfJu047gyY0mkIZWeg6wWBuVYJhAMhJwLAQErOJuuPH/ar6fL29HOBXhW +fUbG6QeQ2hqxpKp0ZJAtJBtI6b7aqeP62UFw3Sg1RFSTOChz+F/KrGZaac0EecRx +b0mhoJTKjQwtPgTIBZIuS0A= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICpjCCAY4CCQDHjcp/tO2FWDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwIBcNMTkwNzI1MTUxMzMzWhgPMjExOTA3MDExNTEzMzNaMBQxEjAQ +BgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AM/ZZm1oIS3lYow11D/fz+ZvRJ7nxK5Hp3LGQSULxp7OsukY9EEiPPKDtYpGK67i +Yi/MS/miqv8YRA7zOw0qyu3ItS5rJPl4k8ZUSwIPCL7UaC67GMbvlr/NAEyAGREA +PhuKP8QEA+w8kGyIQ2OmExg7KVTWWbC/HaY356Fk54I/2wZGZRiBlGLldiXlO0Jf +Ck7huLyIPMbDtrbKEuodcBa6BgocucNq6ric9sEh2mVRXpSpneLgoo+O1r8QUhF5 +RGHdmoF0ub3i4NXvBkzRbIJtxxbR/R3wg2OgskUPs99AmuO28+2euH721Ev4XI8c +BpJkdJhT4q+bkJbBhlcyuucCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAtmoYnUVf +8JJ/tiRlu6XLasMJTVW5jDgcDJ+Da7OTHXSaX83KNShOCoYo/2JXsZtpsQGU6v8H +ofASyDQaebQ5KwCnwph94hZYrnirig+z3cRclvkCb+ohOKME1iAVi4KnkW4UuRgw +wqGKsanx3vHG2stEz3f3b9Az7VpOJq3QJeqklQ45WDs9u/WWWJyTAmechvFS0cP1 +tffCfvCbL7FvnWy60gYNH/OCZ4qixFSTR5ljlrzzHvVJaKT2PGMpMn1Vqn4eJX2a +7ds6dkEns6vTS/b0c0tqGCzD/81/xROfOEeslVEyMny52ER0Pxn9hHSbGlKjXoP1 +zipDSmONaB51lw== +-----END CERTIFICATE----- diff --git a/integration-tests/matrix_is_tester/fakehs.py b/integration-tests/matrix_is_tester/fakehs.py new file mode 100644 index 00000000..ef2d8174 --- /dev/null +++ b/integration-tests/matrix_is_tester/fakehs.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +import base64 +import json +import os +import random +import ssl +from multiprocessing import Process + +from six.moves import BaseHTTPServer, urllib + +shared_fake_hs = None + + +def token_for_random_user(): + """ + Return an OpenID token as would be obtained from the client/server API. + The token will represent a random user account. + """ + num = random.randint(0, 2**32) + user_id = "@user%d:127.0.0.1:4490" % (num,) + return "user:%s" % (base64.b64encode(user_id.encode("UTF-8")).decode("UTF-8"),) + + +def token_for_user(user_id): + """ + Return an OpenID token as would be obtained from the client/server API. + The token will represent the user_id given. + """ + return "user:%s" % (base64.b64encode(user_id.encode("UTF-8")).decode("UTF-8"),) + + +def get_shared_fake_hs(): + """ + Get the shared fake homeserver object, instantiating it if necessary. + """ + global shared_fake_hs + if shared_fake_hs is None: + shared_fake_hs = FakeHomeserver() + shared_fake_hs.launch() + atexit.register(_destroy_shared) + return shared_fake_hs + + +def _destroy_shared(): + shared_fake_hs.tearDown() + + +class _FakeHomeserverRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def do_GET(self): + if self.path.startswith("/_matrix/federation/v1/openid/userinfo"): + parsed = urllib.parse.urlparse(self.path) + params = urllib.parse.parse_qs(parsed.query) + + token = params["access_token"][0] + + if token.startswith("user:"): + userid = base64.b64decode(token.split(":")[1]) + else: + resp = json.dumps( + { + "errcode": "M_UNKNOWN_TOKEN", + "error": "Not a valid token: try again.", + } + ) + self.send_response(401) + self.send_header("Content-Length", len(resp)) + self.end_headers() + + self.wfile.write(resp) + + resp = json.dumps({"sub": userid.decode("UTF-8")}).encode("UTF-8") + + self.send_response(200) + self.send_header("Content-Length", len(resp)) + self.end_headers() + + self.wfile.write(resp) + else: + self.send_response(404) + self.end_headers() + self.wfile.write("Not found") + + def log_message(self, fmt, *args): + # don't print to stdout: it screws up the test output (and we don't really care) + return + + +def _run_http_server(): + cert_file = os.path.join(os.path.dirname(__file__), "fakehs.pem") + + httpd = BaseHTTPServer.HTTPServer( + ("127.0.00.1", 4490), _FakeHomeserverRequestHandler + ) + httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, server_side=True) + httpd.serve_forever() + + +class FakeHomeserver(object): + """ + A class that spawns an HTTP server that looks like a Matrix Homeserver. + Currently just implements the federation OpenID endpoint to validate OpenID tokens. + """ + + def launch(self): + self.process = Process(target=_run_http_server) + self.process.start() + + def get_addr(self): + """ + Returns a host, port tuple representing the address on which the fake homeserver + is listening for requests. + """ + return ("127.0.0.1", 4490) + + def tearDown(self): + self.process.terminate() + + +if __name__ == "__main__": + fakehs = FakeHomeserver() + fakehs.launch() diff --git a/integration-tests/matrix_is_tester/is_api.py b/integration-tests/matrix_is_tester/is_api.py new file mode 100644 index 00000000..fdcc5360 --- /dev/null +++ b/integration-tests/matrix_is_tester/is_api.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import re +import string + +import requests +from twisted.python import log + +from matrix_is_tester.fakehs import token_for_random_user + + +class IsApi(object): + """ + Wrappers around the IS REST API + """ + + def __init__(self, base_url, version, mail_sink): + """ + Args: + base_url (str): The base URL of the IS API to use + version (str): Version of the IS API (eg. 'v1' or 'v2') + XXX: make these either bytes or unicode and fix up the call sites + to use the right one. + mail_sink (MailSink): Mail sink object to use for getting email + authentication tokens. + """ + self.headers = None + + self.version = version + self.base_url = base_url + if version == "v1": + self.apiRoot = base_url + "/_matrix/identity/api/v1" + elif version == "v2": + self.apiRoot = base_url + "/_matrix/identity/v2" + else: + raise Exception("Invalid version: %s" % (version,)) + + self.mail_sink = mail_sink + + # Uses the /register API to create an account. This account will + # be used for all subsequent API calls that requrie auth. + def make_account(self, hs_addr, openid_token=None): + if self.version != "v2": + raise Exception("Only v2 supports authentication") + + if openid_token is None: + openid_token = token_for_random_user() + + body = self.register(":".join([str(x) for x in hs_addr]), openid_token) + self.headers = {"Authorization": "Bearer %s" % (body["token"],)} + + def get_token_from_mail(self): + mail = self.mail_sink.get_mail() + + log.msg("Got email: %r" % (mail,)) + if "data" not in mail: + raise Exception("Mail has no 'data'") + + data = mail["data"] + if isinstance(data, bytes): + data = data.decode("UTF-8") + + matches = re.match(r"<<<(.*)>>>", data) + if not matches.group(1): + raise Exception("Failed to match token from mail") + + return matches.group(1) + + def ping(self): + resp = requests.get(self.apiRoot) + return resp.json() + + def request_email_code(self, address, client_secret, send_attempt): + resp = requests.post( + self.apiRoot + "/validate/email/requestToken", + json={ + "client_secret": client_secret, + "email": address, + "send_attempt": send_attempt, + }, + headers=self.headers, + ) + return resp.json() + + def submit_email_token_via_get(self, sid, client_secret, token): + resp = requests.get( + self.apiRoot + "/validate/email/submitToken", + params={"client_secret": client_secret, "sid": sid, "token": token}, + headers=self.headers, + ) + return resp.content + + def request_and_submit_email_code(self, address): + client_secret = "".join([random.choice(string.digits) for _ in range(16)]) + req_response = self.request_email_code(address, client_secret, 1) + + token = self.get_token_from_mail() + + sid = req_response["sid"] + resp = requests.post( + self.apiRoot + "/validate/email/submitToken", + json={"client_secret": client_secret, "sid": sid, "token": token}, + headers=self.headers, + ) + body = resp.json() + log.msg("submitToken returned %r" % (body,)) + if not body["success"]: + raise Exception("Submit token failed") + return {"sid": sid, "client_secret": client_secret} + + def bind_email(self, sid, client_secret, mxid): + resp = requests.post( + self.apiRoot + "/3pid/bind", + json={"client_secret": client_secret, "sid": sid, "mxid": mxid}, + headers=self.headers, + ) + return resp.json() + + def lookupv1(self, medium, address): + resp = requests.get( + self.apiRoot + "/lookup", + params={"medium": medium, "address": address}, + headers=self.headers, + ) + return resp.json() + + def bulk_lookup(self, threepids): + resp = requests.post( + self.apiRoot + "/bulk_lookup", + json={"threepids": threepids}, + headers=self.headers, + ) + return resp.json() + + def get_validated_threepid(self, sid, client_secret): + resp = requests.get( + self.apiRoot + "/3pid/getValidated3pid", + params={"sid": sid, "client_secret": client_secret}, + headers=self.headers, + ) + return resp.json() + + def store_invite(self, params): + resp = requests.post( + self.apiRoot + "/store-invite", json=params, headers=self.headers + ) + return resp.json() + + def pubkey_is_valid(self, url, pubkey): + resp = requests.get(url, params={"public_key": pubkey}) + return resp.json() + + def get_terms(self): + resp = requests.get(self.apiRoot + "/terms") + return resp.json() + + def agree_to_terms(self, user_accepts): + resp = requests.post( + self.apiRoot + "/terms", + json={"user_accepts": user_accepts}, + headers=self.headers, + ) + return resp.json() + + def get_versions(self): + resp = requests.get(self.base_url + "/versions") + return resp.json() + + def register(self, matrix_server_name, access_token): + resp = requests.post( + self.apiRoot + "/account/register", + json={ + "matrix_server_name": matrix_server_name, + "access_token": access_token, + }, + ) + return resp.json() + + def account(self): + resp = requests.get(self.apiRoot + "/account", headers=self.headers) + return resp.json() + + def logout(self): + resp = requests.post(self.apiRoot + "/account/logout", headers=self.headers) + return resp.json() + + def hash_details(self): + resp = requests.get(self.apiRoot + "/hash_details", headers=self.headers) + return resp.json() + + def hashed_lookup(self, addresses, alg, pepper): + resp = requests.post( + self.apiRoot + "/lookup", + json={"addresses": addresses, "algorithm": alg, "pepper": pepper}, + headers=self.headers, + ) + return resp.json() + + def check_terms_signed(self): + body = self.hash_details() + if "algorithms" in body: + return None + return body diff --git a/integration-tests/matrix_is_tester/launch_is.py b/integration-tests/matrix_is_tester/launch_is.py new file mode 100644 index 00000000..03a4ada4 --- /dev/null +++ b/integration-tests/matrix_is_tester/launch_is.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit + +from matrix_is_tester.launcher import MatrixIsTestLauncher + +launchers = {} + + +def get_or_launch_is(with_terms=False): + global launchers + + key = "withTerms" if with_terms else "noTerms" + + if key not in launchers: + if not launchers: + atexit.register(destroy_all) + + launchers[key] = MatrixIsTestLauncher(with_terms) + launchers[key].launch() + + return launchers[key].get_base_url() + + +def destroy_all(): + global launchers + + for launcher in launchers.values(): + launcher.tearDown() diff --git a/matrix_is_test/launcher.py b/integration-tests/matrix_is_tester/launcher.py similarity index 84% rename from matrix_is_test/launcher.py rename to integration-tests/matrix_is_tester/launcher.py index fa08981a..a3e1f98d 100644 --- a/matrix_is_test/launcher.py +++ b/integration-tests/matrix_is_tester/launcher.py @@ -50,15 +50,19 @@ def __init__(self, with_terms): self.with_terms = with_terms def launch(self): + # integration-tests/matrix_is_test/launcher.py → repo root is ../../ sydent_path = os.path.abspath( os.path.join( os.path.dirname(__file__), "..", + "..", ) ) - testsubject_path = os.path.join( - sydent_path, - "matrix_is_test", + # Templates and terms live alongside this file + testsubject_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + ) ) terms_path = ( os.path.join(testsubject_path, "terms.yaml") if self.with_terms else "" @@ -76,21 +80,11 @@ def launch(self): ) ) - newEnv = os.environ.copy() - newEnv.update( - { - "PYTHONPATH": sydent_path, - } - ) - - stderr_fp = open(os.path.join(testsubject_path, "sydent.stderr"), "w") - - pybin = os.getenv("SYDENT_PYTHON", "python") + stderr_fp = open(os.path.join(self.tmpdir, "sydent.stderr"), "w") self.process = Popen( - args=[pybin, "-m", "sydent.sydent"], + args=["uv", "run", "--project", sydent_path, "sydent"], cwd=self.tmpdir, - env=newEnv, stderr=stderr_fp, ) # XXX: wait for startup in a sensible way diff --git a/integration-tests/matrix_is_tester/mailsink.py b/integration-tests/matrix_is_tester/mailsink.py new file mode 100644 index 00000000..00e67f31 --- /dev/null +++ b/integration-tests/matrix_is_tester/mailsink.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncore +import atexit +import smtpd +from multiprocessing import Process, Queue + +shared_instance = None + + +def get_shared_mailsink(): + global shared_instance + if shared_instance is None: + shared_instance = MailSink() + shared_instance.launch() + atexit.register(destroy_shared) + return shared_instance + + +def destroy_shared(): + global shared_instance + shared_instance.tearDown() + + +class MailSinkSmtpServer(smtpd.SMTPServer): + def __init__(self, localaddr, remoteaddr, q): + smtpd.SMTPServer.__init__(self, localaddr, remoteaddr) + self.queue = q + + def process_message(self, peer, mailfrom, rctpto, data, **kwargs): + self.queue.put( + {"peer": peer, "mailfrom": mailfrom, "rctpto": rctpto, "data": data} + ) + + +def run_mail_sink(q): + MailSinkSmtpServer(("127.0.0.1", 9925), None, q) + asyncore.loop() + + +class MailSink(object): + def launch(self): + self.queue = Queue() + self.process = Process(target=run_mail_sink, args=(self.queue,)) + self.process.start() + + def get_mail(self): + return self.queue.get(timeout=0.5) + + def tearDown(self): + self.process.terminate() + + +if __name__ == "__main__": + ms = MailSink() + ms.launch() + print("%r" % (ms.get_mail(),)) + ms.tearDown() diff --git a/matrix_is_test/res/is-test/invite_template.eml b/integration-tests/matrix_is_tester/res/is-test/invite_template.eml similarity index 100% rename from matrix_is_test/res/is-test/invite_template.eml rename to integration-tests/matrix_is_tester/res/is-test/invite_template.eml diff --git a/matrix_is_test/res/is-test/invite_template.eml.j2 b/integration-tests/matrix_is_tester/res/is-test/invite_template.eml.j2 similarity index 100% rename from matrix_is_test/res/is-test/invite_template.eml.j2 rename to integration-tests/matrix_is_tester/res/is-test/invite_template.eml.j2 diff --git a/matrix_is_test/res/is-test/verification_template.eml b/integration-tests/matrix_is_tester/res/is-test/verification_template.eml similarity index 100% rename from matrix_is_test/res/is-test/verification_template.eml rename to integration-tests/matrix_is_tester/res/is-test/verification_template.eml diff --git a/matrix_is_test/res/is-test/verification_template.eml.j2 b/integration-tests/matrix_is_tester/res/is-test/verification_template.eml.j2 similarity index 100% rename from matrix_is_test/res/is-test/verification_template.eml.j2 rename to integration-tests/matrix_is_tester/res/is-test/verification_template.eml.j2 diff --git a/matrix_is_test/res/is-test/verify_response_template.html b/integration-tests/matrix_is_tester/res/is-test/verify_response_template.html similarity index 100% rename from matrix_is_test/res/is-test/verify_response_template.html rename to integration-tests/matrix_is_tester/res/is-test/verify_response_template.html diff --git a/matrix_is_test/terms.yaml b/integration-tests/matrix_is_tester/terms.yaml similarity index 100% rename from matrix_is_test/terms.yaml rename to integration-tests/matrix_is_tester/terms.yaml diff --git a/integration-tests/matrix_is_tester/test_account.py b/integration-tests/matrix_is_tester/test_account.py new file mode 100755 index 00000000..fbe24c11 --- /dev/null +++ b/integration-tests/matrix_is_tester/test_account.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from matrix_is_tester.fakehs import get_shared_fake_hs, token_for_user +from matrix_is_tester.is_api import IsApi +from matrix_is_tester.launch_is import get_or_launch_is + + +class AccountTest(unittest.TestCase): + def setUp(self): + self.fakeHs = get_shared_fake_hs() + self.fakeHsAddr = self.fakeHs.get_addr() + + def test_account(self): + base_url = get_or_launch_is(False) + api = IsApi(base_url, "v2", None) + api.make_account( + self.fakeHsAddr, token_for_user("@jimmy_account_test:127.0.0.1:4490") + ) + + body = api.account() + + self.assertEqual(body["user_id"], "@jimmy_account_test:127.0.0.1:4490") + + +if __name__ == "__main__": + import sys + + from twisted.python import log + + log.startLogging(sys.stdout) + unittest.main() diff --git a/integration-tests/matrix_is_tester/test_bind_denied.py b/integration-tests/matrix_is_tester/test_bind_denied.py new file mode 100644 index 00000000..b9af1f5a --- /dev/null +++ b/integration-tests/matrix_is_tester/test_bind_denied.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from matrix_is_tester.fakehs import get_shared_fake_hs, token_for_user +from matrix_is_tester.is_api import IsApi +from matrix_is_tester.launch_is import get_or_launch_is +from matrix_is_tester.mailsink import get_shared_mailsink + + +class AccountTest(unittest.TestCase): + def setUp(self): + self.fake_hs = get_shared_fake_hs() + self.fake_hs_addr = self.fake_hs.get_addr() + self.mail_sink = get_shared_mailsink() + + def test_bind_notYourMxid(self): + base_url = get_or_launch_is(False) + api = IsApi(base_url, "v2", self.mail_sink) + api.make_account(self.fake_hs_addr, token_for_user("@bob:127.0.0.1:4490")) + + params = api.request_and_submit_email_code("perfectly_valid_email@nowhere.test") + body = api.bind_email( + params["sid"], params["client_secret"], "@alice:127.0.0.1:4490" + ) + self.assertEquals(body["errcode"], "M_UNAUTHORIZED") + + +if __name__ == "__main__": + import sys + + from twisted.python import log + + log.startLogging(sys.stdout) + unittest.main() diff --git a/integration-tests/matrix_is_tester/test_logout.py b/integration-tests/matrix_is_tester/test_logout.py new file mode 100755 index 00000000..4f4cd271 --- /dev/null +++ b/integration-tests/matrix_is_tester/test_logout.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from matrix_is_tester.fakehs import get_shared_fake_hs +from matrix_is_tester.is_api import IsApi +from matrix_is_tester.launch_is import get_or_launch_is + + +class LogoutTest(unittest.TestCase): + def setUp(self): + self.fakeHs = get_shared_fake_hs() + self.fakeHsAddr = self.fakeHs.get_addr() + + def test_logout(self): + base_url = get_or_launch_is(False) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + body = api.account() + self.assertIn("user_id", body) + + api.logout() + + body = api.account() + self.assertEqual(body["errcode"], "M_UNAUTHORIZED") + + +if __name__ == "__main__": + import sys + + from twisted.python import log + + log.startLogging(sys.stdout) + unittest.main() diff --git a/integration-tests/matrix_is_tester/test_terms.py b/integration-tests/matrix_is_tester/test_terms.py new file mode 100755 index 00000000..e29d19a9 --- /dev/null +++ b/integration-tests/matrix_is_tester/test_terms.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from matrix_is_tester.fakehs import get_shared_fake_hs +from matrix_is_tester.is_api import IsApi +from matrix_is_tester.launch_is import get_or_launch_is + + +class TermsTest(unittest.TestCase): + def setUp(self): + self.fakeHs = get_shared_fake_hs() + self.fakeHsAddr = self.fakeHs.get_addr() + + def test_get_terms(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + + body = api.get_terms() + self.assertIn("policies", body) + self.assertIn("privacy_policy", body["policies"]) + self.assertEquals(body["policies"]["privacy_policy"]["version"], "1.2") + self.assertIn("en", body["policies"]["privacy_policy"]) + self.assertIn("name", body["policies"]["privacy_policy"]["en"]) + self.assertIn("url", body["policies"]["privacy_policy"]["en"]) + self.assertIn("fr", body["policies"]["privacy_policy"]) + self.assertIn("name", body["policies"]["privacy_policy"]["fr"]) + self.assertIn("url", body["policies"]["privacy_policy"]["fr"]) + + self.assertIn("terms_of_service", body["policies"]) + self.assertEquals(body["policies"]["terms_of_service"]["version"], "5.0") + self.assertIn("en", body["policies"]["terms_of_service"]) + self.assertIn("name", body["policies"]["terms_of_service"]["en"]) + self.assertIn("url", body["policies"]["terms_of_service"]["en"]) + self.assertIn("fr", body["policies"]["terms_of_service"]) + self.assertIn("name", body["policies"]["terms_of_service"]["fr"]) + self.assertIn("url", body["policies"]["terms_of_service"]["fr"]) + + def test_agree_to_terms(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + terms_body = api.get_terms() + agree_body = api.agree_to_terms( + [terms_body["policies"]["privacy_policy"]["en"]["url"]] + ) + self.assertEqual(agree_body, {}) + + def test_reject_if_not_authed(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + + err = api.check_terms_signed() + self.assertEquals(err["errcode"], "M_UNAUTHORIZED") + + def test_terms_reject_if_none_agreed(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + err = api.check_terms_signed() + self.assertEquals(err["errcode"], "M_TERMS_NOT_SIGNED") + + def test_terms_reject_if_not_all_agreed(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + terms_body = api.get_terms() + api.agree_to_terms([terms_body["policies"]["privacy_policy"]["en"]["url"]]) + + err = api.check_terms_signed() + self.assertEquals(err["errcode"], "M_TERMS_NOT_SIGNED") + + def test_terms_allow_when_all_agreed(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + terms_body = api.get_terms() + api.agree_to_terms( + [ + terms_body["policies"]["privacy_policy"]["en"]["url"], + terms_body["policies"]["terms_of_service"]["en"]["url"], + ] + ) + + err = api.check_terms_signed() + self.assertIsNone(err) + + def test_terms_allow_mixed_langs(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + terms_body = api.get_terms() + api.agree_to_terms( + [ + terms_body["policies"]["privacy_policy"]["en"]["url"], + terms_body["policies"]["terms_of_service"]["fr"]["url"], + ] + ) + + err = api.check_terms_signed() + self.assertIsNone(err) + + def test_terms_allow_in_separate_calls(self): + base_url = get_or_launch_is(True) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + terms_body = api.get_terms() + api.agree_to_terms([terms_body["policies"]["privacy_policy"]["en"]["url"]]) + api.agree_to_terms([terms_body["policies"]["terms_of_service"]["en"]["url"]]) + + err = api.check_terms_signed() + self.assertIsNone(err) + + def test_terms_no_terms(self): + base_url = get_or_launch_is(False) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + terms_body = api.get_terms() + self.assertEquals(terms_body["policies"], {}) + + def test_terms_allow_if_no_terms(self): + base_url = get_or_launch_is(False) + api = IsApi(base_url, "v2", None) + api.make_account(self.fakeHsAddr) + + err = api.check_terms_signed() + self.assertIsNone(err) + + +if __name__ == "__main__": + import sys + + from twisted.python import log + + log.startLogging(sys.stdout) + unittest.main() diff --git a/integration-tests/matrix_is_tester/test_v1.py b/integration-tests/matrix_is_tester/test_v1.py new file mode 100755 index 00000000..308638df --- /dev/null +++ b/integration-tests/matrix_is_tester/test_v1.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from matrix_is_tester.base_api_test import BaseApiTest + + +class V1Test(BaseApiTest, unittest.TestCase): + API_VERSION = "v1" + + def test_bulk_lookup(self): + params = self.api.request_and_submit_email_code("thing1@nowhere.test") + body = self.api.bind_email( + params["sid"], params["client_secret"], "@thing1:fake.test" + ) + + params = self.api.request_and_submit_email_code("thing2@nowhere.test") + body = self.api.bind_email( + params["sid"], params["client_secret"], "@thing2:fake.test" + ) + + body = self.api.bulk_lookup( + [ + ("email", "thing1@nowhere.test"), + ("email", "thing2@nowhere.test"), + ("email", "thing3@nowhere.test"), + ] + ) + + self.assertIn( + ["email", "thing1@nowhere.test", "@thing1:fake.test"], body["threepids"] + ) + self.assertIn( + ["email", "thing2@nowhere.test", "@thing2:fake.test"], body["threepids"] + ) + self.assertEquals(len(body["threepids"]), 2) + + def test_bind_and_lookup(self): + params = self.api.request_and_submit_email_code("fakeemail3@nowhere.test") + body = self.api.bind_email( + params["sid"], params["client_secret"], "@some_mxid:fake.test" + ) + + self.assertEquals(body["medium"], "email") + self.assertEquals(body["address"], "fakeemail3@nowhere.test") + self.assertEquals(body["mxid"], "@some_mxid:fake.test") + + body2 = self.api.lookupv1("email", "fakeemail3@nowhere.test") + + self.assertEquals(body2["medium"], "email") + self.assertEquals(body2["address"], "fakeemail3@nowhere.test") + self.assertEquals(body2["mxid"], "@some_mxid:fake.test") + + self.assertEquals(body["ts"], body2["ts"]) + self.assertEquals(body["not_before"], body2["not_before"]) + self.assertEquals(body["not_after"], body2["not_after"]) + + +if __name__ == "__main__": + import sys + + from twisted.python import log + + log.startLogging(sys.stdout) + unittest.main() diff --git a/integration-tests/matrix_is_tester/test_v2.py b/integration-tests/matrix_is_tester/test_v2.py new file mode 100755 index 00000000..2ed6e213 --- /dev/null +++ b/integration-tests/matrix_is_tester/test_v2.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from matrix_is_tester.base_api_test import BaseApiTest +from matrix_is_tester.fakehs import get_shared_fake_hs, token_for_user + + +class V2Test(BaseApiTest, unittest.TestCase): + API_VERSION = "v2" + + def setUp(self): + super(V2Test, self).setUp() + + self.fakeHs = get_shared_fake_hs() + self.api.make_account( + self.fakeHs.get_addr(), token_for_user("@commonapitests:127.0.0.1:4490") + ) + + def test_bind_and_lookup(self): + params = self.api.request_and_submit_email_code("fakeemail3@nowhere.test") + body = self.api.bind_email( + params["sid"], params["client_secret"], "@commonapitests:127.0.0.1:4490" + ) + + self.assertEquals(body["medium"], "email") + self.assertEquals(body["address"], "fakeemail3@nowhere.test") + self.assertEquals(body["mxid"], "@commonapitests:127.0.0.1:4490") + + hash_details = self.api.hash_details() + + lookup_str = "%s %s" % ("fakeemail3@nowhere.test", "email") + body2 = self.api.hashed_lookup( + [lookup_str], "none", hash_details["lookup_pepper"] + ) + + self.assertIn(lookup_str, body2["mappings"]) + self.assertEquals( + body2["mappings"][lookup_str], "@commonapitests:127.0.0.1:4490" + ) + + +if __name__ == "__main__": + import sys + + from twisted.python import log + + log.startLogging(sys.stdout) + unittest.main() diff --git a/integration-tests/matrix_is_tester/test_versions.py b/integration-tests/matrix_is_tester/test_versions.py new file mode 100755 index 00000000..893fa01e --- /dev/null +++ b/integration-tests/matrix_is_tester/test_versions.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +# Copyright 2022 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from matrix_is_tester.is_api import IsApi +from matrix_is_tester.launch_is import get_or_launch_is +from matrix_is_tester.mailsink import get_shared_mailsink + + +class VersionsTest(unittest.TestCase): + def setUp(self): + baseUrl = get_or_launch_is() + mailSink = get_shared_mailsink() + + # The API version doesn't matter. + self.api = IsApi(baseUrl, "v1", mailSink) + + def test_versions(self): + body = self.api.get_versions() + + self.assertIn("versions", body) + self.assertIn(["v1.1"], body["versions"]) + + +if __name__ == "__main__": + import sys + + from twisted.python import log + + log.startLogging(sys.stdout) + unittest.main() diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml new file mode 100644 index 00000000..ced190e8 --- /dev/null +++ b/integration-tests/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "matrix-is-tester" +version = "0.1.0" +description = "Black-box integration testing for Matrix Identity Servers" +readme = "README.md" +requires-python = ">=3.10" + +dependencies = [ + "Twisted>=19.2.1", + "requests>=2.22.0", + "six>=1.13.0", +] + +[tool.uv.build-backend] +module-name = "matrix_is_tester" +module-root = "" + +[build-system] +requires = ["uv_build>=0.11.3,<1"] +build-backend = "uv_build" diff --git a/pyproject.toml b/pyproject.toml index c2908893..53aac80e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dev = [ "black>=23.1.0", "ruff==0.0.189", "isort==5.8.0", - "matrix-is-tester @ git+https://github.com/matrix-org/matrix-is-tester@main", + "matrix-is-tester", "mypy>=0.902", "mypy-zope>=0.3.1", "parameterized==0.8.1", @@ -57,6 +57,12 @@ dev = [ "towncrier>=21.9.0", ] +[tool.uv.workspace] +members = ["integration-tests"] + +[tool.uv.sources] +matrix-is-tester = { workspace = true } + [tool.uv.build-backend] module-name = "sydent" module-root = "" @@ -99,6 +105,7 @@ showcontent = true [tool.isort] profile = "black" +known_local_folder = ["tests", "matrix_is_tester"] [tool.black] target-version = ['py310'] diff --git a/scripts/casefold_db.py b/scripts/casefold_db.py index c3750e44..0e3e3281 100755 --- a/scripts/casefold_db.py +++ b/scripts/casefold_db.py @@ -25,6 +25,7 @@ from sydent.util import json_decoder from sydent.util.emailutils import EmailSendException, sendEmail from sydent.util.hash import sha256_and_url_safe_base64 + from tests.utils import ResolvingMemoryReactorClock logger = logging.getLogger("casefold_db") diff --git a/tests/test_auth.py b/tests/test_auth.py index 8f758cb4..cf0533d5 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -10,6 +10,7 @@ from twisted.trial import unittest from sydent.http.auth import tokenFromRequest + from tests.utils import make_request, make_sydent diff --git a/tests/test_blacklisting.py b/tests/test_blacklisting.py index 46c9357a..571cb1ad 100644 --- a/tests/test_blacklisting.py +++ b/tests/test_blacklisting.py @@ -16,6 +16,7 @@ from sydent.http.blacklisting_reactor import BlacklistingReactorWrapper from sydent.http.srvresolver import Server + from tests.utils import AsyncMock, make_request, make_sydent diff --git a/tests/test_casefold_migration.py b/tests/test_casefold_migration.py index 9e6e0b51..2c63b29c 100644 --- a/tests/test_casefold_migration.py +++ b/tests/test_casefold_migration.py @@ -18,6 +18,7 @@ ) from sydent.util import json_decoder from sydent.util.emailutils import sendEmail + from tests.utils import make_sydent diff --git a/tests/test_email.py b/tests/test_email.py index 7c63c2c3..b5a21b0e 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -13,6 +13,7 @@ from twisted.trial import unittest from sydent.types import JsonDict + from tests.utils import make_request, make_sydent diff --git a/tests/test_invites.py b/tests/test_invites.py index 1a7e32d8..7dd0bf75 100644 --- a/tests/test_invites.py +++ b/tests/test_invites.py @@ -6,6 +6,7 @@ from sydent.db.invite_tokens import JoinTokenStore from sydent.http.httpclient import FederationHttpClient from sydent.http.servlets.store_invite_servlet import StoreInviteServlet + from tests.utils import make_request, make_sydent diff --git a/tests/test_jinja_templates.py b/tests/test_jinja_templates.py index ca11872e..d0485b5a 100644 --- a/tests/test_jinja_templates.py +++ b/tests/test_jinja_templates.py @@ -14,6 +14,7 @@ from twisted.trial import unittest from sydent.util.emailutils import sendEmail + from tests.utils import make_sydent diff --git a/tests/test_msisdn.py b/tests/test_msisdn.py index 24dd39f1..bd5f68a0 100644 --- a/tests/test_msisdn.py +++ b/tests/test_msisdn.py @@ -14,6 +14,7 @@ from twisted.trial import unittest from sydent.types import JsonDict + from tests.utils import make_request, make_sydent diff --git a/tests/test_replication.py b/tests/test_replication.py index b06d3720..06651237 100644 --- a/tests/test_replication.py +++ b/tests/test_replication.py @@ -7,6 +7,7 @@ from sydent.threepid import ThreepidAssociation from sydent.threepid.signer import Signer + from tests.utils import make_request, make_sydent diff --git a/tests/test_store_invite.py b/tests/test_store_invite.py index 60f10856..904b9a02 100644 --- a/tests/test_store_invite.py +++ b/tests/test_store_invite.py @@ -12,6 +12,7 @@ from twisted.trial import unittest from sydent.users.accounts import Account + from tests.utils import make_request, make_sydent diff --git a/uv.lock b/uv.lock index 50aef255..87bc5aeb 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.10, <4" +[manifest] +members = [ + "matrix-is-tester", + "matrix-sydent", +] + [[package]] name = "attrs" version = "26.1.0" @@ -610,14 +616,21 @@ wheels = [ [[package]] name = "matrix-is-tester" -version = "0.1" -source = { git = "https://github.com/matrix-org/matrix-is-tester?rev=main#cfc3bf0b998c5d0ae91d0f2f4f6cb007a18833aa" } +version = "0.1.0" +source = { editable = "integration-tests" } dependencies = [ { name = "requests" }, { name = "six" }, { name = "twisted" }, ] +[package.metadata] +requires-dist = [ + { name = "requests", specifier = ">=2.22.0" }, + { name = "six", specifier = ">=1.13.0" }, + { name = "twisted", specifier = ">=19.2.1" }, +] + [[package]] name = "matrix-sydent" version = "2.7.0" @@ -691,7 +704,7 @@ provides-extras = ["sentry", "prometheus"] dev = [ { name = "black", specifier = ">=23.1.0" }, { name = "isort", specifier = "==5.8.0" }, - { name = "matrix-is-tester", git = "https://github.com/matrix-org/matrix-is-tester?rev=main" }, + { name = "matrix-is-tester", editable = "integration-tests" }, { name = "mypy", specifier = ">=0.902" }, { name = "mypy-zope", specifier = ">=0.3.1" }, { name = "parameterized", specifier = "==0.8.1" }, From e99d0a51484120942a213f0cddd2bb19dbdf8b2a Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Sat, 4 Apr 2026 14:27:48 +0200 Subject: [PATCH 2/5] Drop six, fix ssl.wrap_socket and IP typo in integration tests --- integration-tests/matrix_is_tester/fakehs.py | 22 ++++++++++---------- integration-tests/pyproject.toml | 1 - uv.lock | 11 ---------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/integration-tests/matrix_is_tester/fakehs.py b/integration-tests/matrix_is_tester/fakehs.py index ef2d8174..f58d5cbd 100644 --- a/integration-tests/matrix_is_tester/fakehs.py +++ b/integration-tests/matrix_is_tester/fakehs.py @@ -20,9 +20,9 @@ import os import random import ssl +from http.server import BaseHTTPRequestHandler, HTTPServer from multiprocessing import Process - -from six.moves import BaseHTTPServer, urllib +from urllib.parse import parse_qs, urlparse shared_fake_hs = None @@ -61,11 +61,11 @@ def _destroy_shared(): shared_fake_hs.tearDown() -class _FakeHomeserverRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): +class _FakeHomeserverRequestHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path.startswith("/_matrix/federation/v1/openid/userinfo"): - parsed = urllib.parse.urlparse(self.path) - params = urllib.parse.parse_qs(parsed.query) + parsed = urlparse(self.path) + params = parse_qs(parsed.query) token = params["access_token"][0] @@ -94,7 +94,7 @@ def do_GET(self): else: self.send_response(404) self.end_headers() - self.wfile.write("Not found") + self.wfile.write(b"Not found") def log_message(self, fmt, *args): # don't print to stdout: it screws up the test output (and we don't really care) @@ -104,14 +104,14 @@ def log_message(self, fmt, *args): def _run_http_server(): cert_file = os.path.join(os.path.dirname(__file__), "fakehs.pem") - httpd = BaseHTTPServer.HTTPServer( - ("127.0.00.1", 4490), _FakeHomeserverRequestHandler - ) - httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, server_side=True) + httpd = HTTPServer(("127.0.0.1", 4490), _FakeHomeserverRequestHandler) + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(cert_file) + httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) httpd.serve_forever() -class FakeHomeserver(object): +class FakeHomeserver: """ A class that spawns an HTTP server that looks like a Matrix Homeserver. Currently just implements the federation OpenID endpoint to validate OpenID tokens. diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index ced190e8..efa2204f 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -8,7 +8,6 @@ requires-python = ">=3.10" dependencies = [ "Twisted>=19.2.1", "requests>=2.22.0", - "six>=1.13.0", ] [tool.uv.build-backend] diff --git a/uv.lock b/uv.lock index 87bc5aeb..e2a915b7 100644 --- a/uv.lock +++ b/uv.lock @@ -620,14 +620,12 @@ version = "0.1.0" source = { editable = "integration-tests" } dependencies = [ { name = "requests" }, - { name = "six" }, { name = "twisted" }, ] [package.metadata] requires-dist = [ { name = "requests", specifier = ">=2.22.0" }, - { name = "six", specifier = ">=1.13.0" }, { name = "twisted", specifier = ">=19.2.1" }, ] @@ -1109,15 +1107,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/dc/f2/2b3c5574ab77e086597cdc85781fa1d2ef1b4e55bd53d47308bbd2ad7596/signedjson-1.1.1.tar.gz", hash = "sha256:350586e7570ba208f7729dcda09d43f554ead0207a15e3e3695533ef3f720009", size = 10906, upload-time = "2020-03-27T19:49:12.654Z" } -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" From 15f545b2b8b69a09453c666aa9b0fa34d86fd9c5 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Sat, 4 Apr 2026 14:29:00 +0200 Subject: [PATCH 3/5] Replace asyncore/smtpd with aiosmtpd in integration tests --- .../matrix_is_tester/mailsink.py | 42 ++++++++++++------- integration-tests/pyproject.toml | 1 + uv.lock | 24 +++++++++++ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/integration-tests/matrix_is_tester/mailsink.py b/integration-tests/matrix_is_tester/mailsink.py index 00e67f31..88396017 100644 --- a/integration-tests/matrix_is_tester/mailsink.py +++ b/integration-tests/matrix_is_tester/mailsink.py @@ -14,11 +14,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncore +import asyncio import atexit -import smtpd from multiprocessing import Process, Queue +from aiosmtpd.controller import Controller + shared_instance = None @@ -36,30 +37,43 @@ def destroy_shared(): shared_instance.tearDown() -class MailSinkSmtpServer(smtpd.SMTPServer): - def __init__(self, localaddr, remoteaddr, q): - smtpd.SMTPServer.__init__(self, localaddr, remoteaddr) - self.queue = q +class _MailSinkHandler: + def __init__(self, queue): + self.queue = queue - def process_message(self, peer, mailfrom, rctpto, data, **kwargs): + async def handle_DATA(self, server, session, envelope): self.queue.put( - {"peer": peer, "mailfrom": mailfrom, "rctpto": rctpto, "data": data} + { + "peer": session.peer, + "mailfrom": envelope.mail_from, + "rctpto": envelope.rcpt_tos, + "data": envelope.content.decode("utf-8", errors="replace"), + } ) + return "250 OK" -def run_mail_sink(q): - MailSinkSmtpServer(("127.0.0.1", 9925), None, q) - asyncore.loop() +def _run_mail_sink(q): + handler = _MailSinkHandler(q) + controller = Controller(handler, hostname="127.0.0.1", port=9925) + controller.start() + # Block forever (the controller runs in a thread) + try: + asyncio.get_event_loop().run_forever() + except KeyboardInterrupt: + pass + finally: + controller.stop() -class MailSink(object): +class MailSink: def launch(self): self.queue = Queue() - self.process = Process(target=run_mail_sink, args=(self.queue,)) + self.process = Process(target=_run_mail_sink, args=(self.queue,)) self.process.start() def get_mail(self): - return self.queue.get(timeout=0.5) + return self.queue.get(timeout=2.0) def tearDown(self): self.process.terminate() diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index efa2204f..c5180e7f 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ + "aiosmtpd>=1.4", "Twisted>=19.2.1", "requests>=2.22.0", ] diff --git a/uv.lock b/uv.lock index e2a915b7..2ef94fb2 100644 --- a/uv.lock +++ b/uv.lock @@ -8,6 +8,28 @@ members = [ "matrix-sydent", ] +[[package]] +name = "aiosmtpd" +version = "1.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "atpublic" }, + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/ca/b2b7cc880403ef24be77383edaadfcf0098f5d7b9ddbf3e2c17ef0a6af0d/aiosmtpd-1.4.6.tar.gz", hash = "sha256:5a811826e1a5a06c25ebc3e6c4a704613eb9a1bcf6b78428fbe865f4f6c9a4b8", size = 152775, upload-time = "2024-05-18T11:37:50.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/39/d401756df60a8344848477d54fdf4ce0f50531f6149f3b8eaae9c06ae3dc/aiosmtpd-1.4.6-py3-none-any.whl", hash = "sha256:72c99179ba5aa9ae0abbda6994668239b64a5ce054471955fe75f581d2592475", size = 154263, upload-time = "2024-05-18T11:37:47.877Z" }, +] + +[[package]] +name = "atpublic" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/05/e2e131a0debaf0f01b8a1b586f5f11713f6affc3e711b406f15f11eafc92/atpublic-7.0.0.tar.gz", hash = "sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e", size = 17801, upload-time = "2025-11-29T05:56:45.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c0/271f3e1e3502a8decb8ee5c680dbed2d8dc2cd504f5e20f7ed491d5f37e1/atpublic-7.0.0-py3-none-any.whl", hash = "sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b", size = 6421, upload-time = "2025-11-29T05:56:44.604Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -619,12 +641,14 @@ name = "matrix-is-tester" version = "0.1.0" source = { editable = "integration-tests" } dependencies = [ + { name = "aiosmtpd" }, { name = "requests" }, { name = "twisted" }, ] [package.metadata] requires-dist = [ + { name = "aiosmtpd", specifier = ">=1.4" }, { name = "requests", specifier = ">=2.22.0" }, { name = "twisted", specifier = ">=19.2.1" }, ] From f8f403b9dd7e9fc4c1e9d9c0b179be3f54329cdb Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Sat, 4 Apr 2026 14:29:43 +0200 Subject: [PATCH 4/5] Drop Twisted logging, fix deprecated assertions in integration tests --- .../matrix_is_tester/base_api_test.py | 46 +++++++++---------- integration-tests/matrix_is_tester/is_api.py | 10 ++-- .../matrix_is_tester/test_account.py | 5 -- .../matrix_is_tester/test_bind_denied.py | 7 +-- .../matrix_is_tester/test_logout.py | 5 -- .../matrix_is_tester/test_terms.py | 17 +++---- integration-tests/matrix_is_tester/test_v1.py | 25 ++++------ integration-tests/matrix_is_tester/test_v2.py | 13 ++---- .../matrix_is_tester/test_versions.py | 7 +-- integration-tests/pyproject.toml | 1 - uv.lock | 2 - 11 files changed, 49 insertions(+), 89 deletions(-) diff --git a/integration-tests/matrix_is_tester/base_api_test.py b/integration-tests/matrix_is_tester/base_api_test.py index f32b1767..a12e2dfd 100644 --- a/integration-tests/matrix_is_tester/base_api_test.py +++ b/integration-tests/matrix_is_tester/base_api_test.py @@ -17,16 +17,14 @@ # limitations under the License. import json - -# These are standard python unit tests, but are generally intended -# to be run with trial. Trial doesn't capture logging nicely if you -# use python 'logging': it only works if you use Twisted's own. -from twisted.python import log +import logging from matrix_is_tester.is_api import IsApi from matrix_is_tester.launch_is import get_or_launch_is from matrix_is_tester.mailsink import get_shared_mailsink +logger = logging.getLogger(__name__) + class BaseApiTest(object): """ @@ -43,11 +41,11 @@ def setUp(self): def test_ping(self): body = self.api.ping() - self.assertEquals(body, {}) + self.assertEqual(body, {}) def test_request_email_code(self): body = self.api.request_email_code("fakeemail1@nowhere.test", "sekrit", 1) - log.msg("Got response %r" % (body,)) + logger.info("Got response %r" % (body,)) self.assertIn("sid", body) self.mailSink.get_mail() @@ -55,7 +53,7 @@ def test_reject_invalid_email(self): body = self.api.request_email_code( "fakeemail1@nowhere.test@elsewhere.test", "sekrit", 1 ) - self.assertEquals(body["errcode"], "M_INVALID_EMAIL") + self.assertEqual(body["errcode"], "M_INVALID_EMAIL") def test_submit_email_code(self): self.api.request_and_submit_email_code("fakeemail2@nowhere.test") @@ -69,12 +67,12 @@ def test_submit_email_code_get(self): token = self.api.get_token_from_mail() body = self.api.submit_email_token_via_get(sid, "verysekrit", token) - self.assertEquals(body, b"matrix_is_tester:email_submit_get_response\n") + self.assertEqual(body, b"matrix_is_tester:email_submit_get_response\n") body = self.api.get_validated_threepid(sid, "verysekrit") - self.assertEquals(body["medium"], "email") - self.assertEquals(body["address"], "steve@nowhere.test") + self.assertEqual(body["medium"], "email") + self.assertEqual(body["address"], "steve@nowhere.test") def test_unverified_bind(self): req_code_body = self.api.request_email_code( @@ -85,15 +83,15 @@ def test_unverified_bind(self): body = self.api.bind_email( req_code_body["sid"], "sekrit", "@commonapitests:127.0.0.1:4490" ) - self.assertEquals(body["errcode"], "M_SESSION_NOT_VALIDATED") + self.assertEqual(body["errcode"], "M_SESSION_NOT_VALIDATED") def test_get_validated_threepid(self): params = self.api.request_and_submit_email_code("fakeemail4@nowhere.test") body = self.api.get_validated_threepid(params["sid"], params["client_secret"]) - self.assertEquals(body["medium"], "email") - self.assertEquals(body["address"], "fakeemail4@nowhere.test") + self.assertEqual(body["medium"], "email") + self.assertEqual(body["address"], "fakeemail4@nowhere.test") def test_get_validated_threepid_not_validated(self): req_code_body = self.api.request_email_code( @@ -104,7 +102,7 @@ def test_get_validated_threepid_not_validated(self): self.mailSink.get_mail() get_val_body = self.api.get_validated_threepid(req_code_body["sid"], "sekrit") - self.assertEquals(get_val_body["errcode"], "M_SESSION_NOT_VALIDATED") + self.assertEqual(get_val_body["errcode"], "M_SESSION_NOT_VALIDATED") def test_store_invite(self): body = self.api.store_invite( @@ -132,16 +130,14 @@ def test_store_invite(self): self.assertTrue(is_valid_body["valid"]) mail = self.mailSink.get_mail() - log.msg("Got email (invite): %r" % (mail,)) + logger.info("Got email (invite): %r" % (mail,)) mail_object = json.loads(mail["data"]) - self.assertEquals(mail_object["token"], body["token"]) - self.assertEquals(mail_object["room_alias"], "#alias:fake.test") - self.assertEquals(mail_object["room_avatar_url"], "mxc://fake.test/roomavatar") - self.assertEquals(mail_object["room_name"], "my excellent room") - self.assertEquals(mail_object["sender_display_name"], "Ian Sender") - self.assertEquals( - mail_object["sender_avatar_url"], "mxc://fake.test/iansavatar" - ) + self.assertEqual(mail_object["token"], body["token"]) + self.assertEqual(mail_object["room_alias"], "#alias:fake.test") + self.assertEqual(mail_object["room_avatar_url"], "mxc://fake.test/roomavatar") + self.assertEqual(mail_object["room_name"], "my excellent room") + self.assertEqual(mail_object["sender_display_name"], "Ian Sender") + self.assertEqual(mail_object["sender_avatar_url"], "mxc://fake.test/iansavatar") def test_store_invite_bound_threepid(self): params = self.api.request_and_submit_email_code("already_here@fake.test") @@ -157,4 +153,4 @@ def test_store_invite_bound_threepid(self): "sender": "@commonapitests:127.0.0.1:4490", } ) - self.assertEquals(body["errcode"], "M_THREEPID_IN_USE") + self.assertEqual(body["errcode"], "M_THREEPID_IN_USE") diff --git a/integration-tests/matrix_is_tester/is_api.py b/integration-tests/matrix_is_tester/is_api.py index fdcc5360..9ac41f61 100644 --- a/integration-tests/matrix_is_tester/is_api.py +++ b/integration-tests/matrix_is_tester/is_api.py @@ -16,15 +16,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import random import re import string import requests -from twisted.python import log from matrix_is_tester.fakehs import token_for_random_user +logger = logging.getLogger(__name__) + class IsApi(object): """ @@ -69,7 +71,7 @@ def make_account(self, hs_addr, openid_token=None): def get_token_from_mail(self): mail = self.mail_sink.get_mail() - log.msg("Got email: %r" % (mail,)) + logger.info("Got email: %r" % (mail,)) if "data" not in mail: raise Exception("Mail has no 'data'") @@ -120,7 +122,7 @@ def request_and_submit_email_code(self, address): headers=self.headers, ) body = resp.json() - log.msg("submitToken returned %r" % (body,)) + logger.info("submitToken returned %r" % (body,)) if not body["success"]: raise Exception("Submit token failed") return {"sid": sid, "client_secret": client_secret} @@ -180,7 +182,7 @@ def agree_to_terms(self, user_accepts): return resp.json() def get_versions(self): - resp = requests.get(self.base_url + "/versions") + resp = requests.get(self.base_url + "/_matrix/identity/versions") return resp.json() def register(self, matrix_server_name, access_token): diff --git a/integration-tests/matrix_is_tester/test_account.py b/integration-tests/matrix_is_tester/test_account.py index fbe24c11..ed859bd0 100755 --- a/integration-tests/matrix_is_tester/test_account.py +++ b/integration-tests/matrix_is_tester/test_account.py @@ -41,9 +41,4 @@ def test_account(self): if __name__ == "__main__": - import sys - - from twisted.python import log - - log.startLogging(sys.stdout) unittest.main() diff --git a/integration-tests/matrix_is_tester/test_bind_denied.py b/integration-tests/matrix_is_tester/test_bind_denied.py index b9af1f5a..d40bd5af 100644 --- a/integration-tests/matrix_is_tester/test_bind_denied.py +++ b/integration-tests/matrix_is_tester/test_bind_denied.py @@ -39,13 +39,8 @@ def test_bind_notYourMxid(self): body = api.bind_email( params["sid"], params["client_secret"], "@alice:127.0.0.1:4490" ) - self.assertEquals(body["errcode"], "M_UNAUTHORIZED") + self.assertEqual(body["errcode"], "M_UNAUTHORIZED") if __name__ == "__main__": - import sys - - from twisted.python import log - - log.startLogging(sys.stdout) unittest.main() diff --git a/integration-tests/matrix_is_tester/test_logout.py b/integration-tests/matrix_is_tester/test_logout.py index 4f4cd271..d40e8d20 100755 --- a/integration-tests/matrix_is_tester/test_logout.py +++ b/integration-tests/matrix_is_tester/test_logout.py @@ -43,9 +43,4 @@ def test_logout(self): if __name__ == "__main__": - import sys - - from twisted.python import log - - log.startLogging(sys.stdout) unittest.main() diff --git a/integration-tests/matrix_is_tester/test_terms.py b/integration-tests/matrix_is_tester/test_terms.py index e29d19a9..485d0392 100755 --- a/integration-tests/matrix_is_tester/test_terms.py +++ b/integration-tests/matrix_is_tester/test_terms.py @@ -35,7 +35,7 @@ def test_get_terms(self): body = api.get_terms() self.assertIn("policies", body) self.assertIn("privacy_policy", body["policies"]) - self.assertEquals(body["policies"]["privacy_policy"]["version"], "1.2") + self.assertEqual(body["policies"]["privacy_policy"]["version"], "1.2") self.assertIn("en", body["policies"]["privacy_policy"]) self.assertIn("name", body["policies"]["privacy_policy"]["en"]) self.assertIn("url", body["policies"]["privacy_policy"]["en"]) @@ -44,7 +44,7 @@ def test_get_terms(self): self.assertIn("url", body["policies"]["privacy_policy"]["fr"]) self.assertIn("terms_of_service", body["policies"]) - self.assertEquals(body["policies"]["terms_of_service"]["version"], "5.0") + self.assertEqual(body["policies"]["terms_of_service"]["version"], "5.0") self.assertIn("en", body["policies"]["terms_of_service"]) self.assertIn("name", body["policies"]["terms_of_service"]["en"]) self.assertIn("url", body["policies"]["terms_of_service"]["en"]) @@ -68,7 +68,7 @@ def test_reject_if_not_authed(self): api = IsApi(base_url, "v2", None) err = api.check_terms_signed() - self.assertEquals(err["errcode"], "M_UNAUTHORIZED") + self.assertEqual(err["errcode"], "M_UNAUTHORIZED") def test_terms_reject_if_none_agreed(self): base_url = get_or_launch_is(True) @@ -76,7 +76,7 @@ def test_terms_reject_if_none_agreed(self): api.make_account(self.fakeHsAddr) err = api.check_terms_signed() - self.assertEquals(err["errcode"], "M_TERMS_NOT_SIGNED") + self.assertEqual(err["errcode"], "M_TERMS_NOT_SIGNED") def test_terms_reject_if_not_all_agreed(self): base_url = get_or_launch_is(True) @@ -87,7 +87,7 @@ def test_terms_reject_if_not_all_agreed(self): api.agree_to_terms([terms_body["policies"]["privacy_policy"]["en"]["url"]]) err = api.check_terms_signed() - self.assertEquals(err["errcode"], "M_TERMS_NOT_SIGNED") + self.assertEqual(err["errcode"], "M_TERMS_NOT_SIGNED") def test_terms_allow_when_all_agreed(self): base_url = get_or_launch_is(True) @@ -139,7 +139,7 @@ def test_terms_no_terms(self): api.make_account(self.fakeHsAddr) terms_body = api.get_terms() - self.assertEquals(terms_body["policies"], {}) + self.assertEqual(terms_body["policies"], {}) def test_terms_allow_if_no_terms(self): base_url = get_or_launch_is(False) @@ -151,9 +151,4 @@ def test_terms_allow_if_no_terms(self): if __name__ == "__main__": - import sys - - from twisted.python import log - - log.startLogging(sys.stdout) unittest.main() diff --git a/integration-tests/matrix_is_tester/test_v1.py b/integration-tests/matrix_is_tester/test_v1.py index 308638df..99ba7b61 100755 --- a/integration-tests/matrix_is_tester/test_v1.py +++ b/integration-tests/matrix_is_tester/test_v1.py @@ -49,7 +49,7 @@ def test_bulk_lookup(self): self.assertIn( ["email", "thing2@nowhere.test", "@thing2:fake.test"], body["threepids"] ) - self.assertEquals(len(body["threepids"]), 2) + self.assertEqual(len(body["threepids"]), 2) def test_bind_and_lookup(self): params = self.api.request_and_submit_email_code("fakeemail3@nowhere.test") @@ -57,25 +57,20 @@ def test_bind_and_lookup(self): params["sid"], params["client_secret"], "@some_mxid:fake.test" ) - self.assertEquals(body["medium"], "email") - self.assertEquals(body["address"], "fakeemail3@nowhere.test") - self.assertEquals(body["mxid"], "@some_mxid:fake.test") + self.assertEqual(body["medium"], "email") + self.assertEqual(body["address"], "fakeemail3@nowhere.test") + self.assertEqual(body["mxid"], "@some_mxid:fake.test") body2 = self.api.lookupv1("email", "fakeemail3@nowhere.test") - self.assertEquals(body2["medium"], "email") - self.assertEquals(body2["address"], "fakeemail3@nowhere.test") - self.assertEquals(body2["mxid"], "@some_mxid:fake.test") + self.assertEqual(body2["medium"], "email") + self.assertEqual(body2["address"], "fakeemail3@nowhere.test") + self.assertEqual(body2["mxid"], "@some_mxid:fake.test") - self.assertEquals(body["ts"], body2["ts"]) - self.assertEquals(body["not_before"], body2["not_before"]) - self.assertEquals(body["not_after"], body2["not_after"]) + self.assertEqual(body["ts"], body2["ts"]) + self.assertEqual(body["not_before"], body2["not_before"]) + self.assertEqual(body["not_after"], body2["not_after"]) if __name__ == "__main__": - import sys - - from twisted.python import log - - log.startLogging(sys.stdout) unittest.main() diff --git a/integration-tests/matrix_is_tester/test_v2.py b/integration-tests/matrix_is_tester/test_v2.py index 2ed6e213..a0d3642c 100755 --- a/integration-tests/matrix_is_tester/test_v2.py +++ b/integration-tests/matrix_is_tester/test_v2.py @@ -39,9 +39,9 @@ def test_bind_and_lookup(self): params["sid"], params["client_secret"], "@commonapitests:127.0.0.1:4490" ) - self.assertEquals(body["medium"], "email") - self.assertEquals(body["address"], "fakeemail3@nowhere.test") - self.assertEquals(body["mxid"], "@commonapitests:127.0.0.1:4490") + self.assertEqual(body["medium"], "email") + self.assertEqual(body["address"], "fakeemail3@nowhere.test") + self.assertEqual(body["mxid"], "@commonapitests:127.0.0.1:4490") hash_details = self.api.hash_details() @@ -51,15 +51,10 @@ def test_bind_and_lookup(self): ) self.assertIn(lookup_str, body2["mappings"]) - self.assertEquals( + self.assertEqual( body2["mappings"][lookup_str], "@commonapitests:127.0.0.1:4490" ) if __name__ == "__main__": - import sys - - from twisted.python import log - - log.startLogging(sys.stdout) unittest.main() diff --git a/integration-tests/matrix_is_tester/test_versions.py b/integration-tests/matrix_is_tester/test_versions.py index 893fa01e..69148c37 100755 --- a/integration-tests/matrix_is_tester/test_versions.py +++ b/integration-tests/matrix_is_tester/test_versions.py @@ -35,13 +35,8 @@ def test_versions(self): body = self.api.get_versions() self.assertIn("versions", body) - self.assertIn(["v1.1"], body["versions"]) + self.assertIn("v1.1", body["versions"]) if __name__ == "__main__": - import sys - - from twisted.python import log - - log.startLogging(sys.stdout) unittest.main() diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index c5180e7f..d44ecd61 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -7,7 +7,6 @@ requires-python = ">=3.10" dependencies = [ "aiosmtpd>=1.4", - "Twisted>=19.2.1", "requests>=2.22.0", ] diff --git a/uv.lock b/uv.lock index 2ef94fb2..b903a1a8 100644 --- a/uv.lock +++ b/uv.lock @@ -643,14 +643,12 @@ source = { editable = "integration-tests" } dependencies = [ { name = "aiosmtpd" }, { name = "requests" }, - { name = "twisted" }, ] [package.metadata] requires-dist = [ { name = "aiosmtpd", specifier = ">=1.4" }, { name = "requests", specifier = ">=2.22.0" }, - { name = "twisted", specifier = ">=19.2.1" }, ] [[package]] From 7ff4744f1c5944d5665a958786ad606a75498e96 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Sat, 4 Apr 2026 17:11:14 +0200 Subject: [PATCH 5/5] Newsfile for #622. --- changelog.d/622.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/622.misc diff --git a/changelog.d/622.misc b/changelog.d/622.misc new file mode 100644 index 00000000..8b7c154a --- /dev/null +++ b/changelog.d/622.misc @@ -0,0 +1 @@ +Absorb matrix-is-tester as a workspace member and modernise for Python 3.12+.