From 00b25f5e028a2eb5deeccd3a54e34c5ef6540af7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 3 Jan 2025 18:48:25 +0000 Subject: [PATCH 01/93] Switch to TestContainers for manging services in Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 2 + .../app-loading/guest-registration.spec.ts | 6 +- playwright/e2e/crypto/backups.spec.ts | 8 +- playwright/e2e/crypto/dehydration.spec.ts | 10 +- playwright/e2e/crypto/utils.ts | 2 +- .../forgot-password/forgot-password.spec.ts | 18 +- playwright/e2e/login/consent.spec.ts | 3 +- playwright/e2e/login/login.spec.ts | 35 +- playwright/e2e/login/overwrite_login.spec.ts | 2 +- playwright/e2e/login/soft_logout.spec.ts | 10 +- playwright/e2e/login/utils.ts | 5 +- playwright/e2e/oidc/index.ts | 178 +++++- playwright/e2e/oidc/oidc-native.spec.ts | 10 +- playwright/e2e/register/email.spec.ts | 17 +- playwright/e2e/register/register.spec.ts | 9 +- .../e2e/sliding-sync/sliding-sync.spec.ts | 24 +- playwright/element-web-test.ts | 70 +-- playwright/pages/bot.ts | 6 +- playwright/pages/crypto.ts | 2 +- playwright/plugins/docker/index.ts | 151 ----- .../plugins/homeserver/dendrite/index.ts | 169 ++---- .../dendrite/templates/default/dendrite.yaml | 378 ------------ playwright/plugins/homeserver/index.ts | 31 +- .../homeserver/synapse/consentHomeserver.ts | 56 ++ .../homeserver/synapse/emailHomeserver.ts | 28 + .../plugins/homeserver/synapse/index.ts | 239 -------- .../synapse/legacyOAuthHomeserver.ts | 48 ++ .../res/templates/privacy/en/1.0.html | 0 .../res/templates/privacy/en/success.html | 0 .../synapse/templates/COPYME/README.md | 3 - .../synapse/templates/COPYME/homeserver.yaml | 72 --- .../synapse/templates/COPYME/log.config | 50 -- .../synapse/templates/consent/README.md | 1 - .../synapse/templates/consent/homeserver.yaml | 84 --- .../synapse/templates/consent/log.config | 50 -- .../synapse/templates/default/README.md | 1 - .../synapse/templates/default/homeserver.yaml | 106 ---- .../synapse/templates/default/log.config | 50 -- .../synapse/templates/dehydration/README.md | 1 - .../templates/dehydration/homeserver.yaml | 102 ---- .../synapse/templates/dehydration/log.config | 50 -- .../synapse/templates/email/README.md | 1 - .../synapse/templates/email/homeserver.yaml | 44 -- .../synapse/templates/email/log.config | 50 -- .../synapse/templates/guest-enabled/README.md | 1 - .../templates/guest-enabled/homeserver.yaml | 105 ---- .../templates/guest-enabled/log.config | 50 -- .../synapse/templates/mas-oidc/README.md | 1 - .../templates/mas-oidc/homeserver.yaml | 194 ------ .../synapse/templates/mas-oidc/log.config | 50 -- playwright/plugins/mailhog/index.ts | 47 -- .../matrix-authentication-service/config.yaml | 153 ----- .../matrix-authentication-service/index.ts | 151 ----- playwright/plugins/postgres/index.ts | 66 --- .../plugins/sliding-sync-proxy/index.ts | 77 --- playwright/services.ts | 113 ++++ playwright/testcontainers/dendrite.ts | 280 +++++++++ playwright/testcontainers/mas.ts | 206 +++++++ playwright/testcontainers/synapse.ts | 323 ++++++++++ yarn.lock | 550 +++++++++++++++++- 60 files changed, 1857 insertions(+), 2692 deletions(-) delete mode 100644 playwright/plugins/docker/index.ts delete mode 100644 playwright/plugins/homeserver/dendrite/templates/default/dendrite.yaml create mode 100644 playwright/plugins/homeserver/synapse/consentHomeserver.ts create mode 100644 playwright/plugins/homeserver/synapse/emailHomeserver.ts delete mode 100644 playwright/plugins/homeserver/synapse/index.ts create mode 100644 playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts rename playwright/plugins/homeserver/synapse/{templates/consent => }/res/templates/privacy/en/1.0.html (100%) rename playwright/plugins/homeserver/synapse/{templates/consent => }/res/templates/privacy/en/success.html (100%) delete mode 100644 playwright/plugins/homeserver/synapse/templates/COPYME/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/COPYME/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/COPYME/log.config delete mode 100644 playwright/plugins/homeserver/synapse/templates/consent/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/consent/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/consent/log.config delete mode 100644 playwright/plugins/homeserver/synapse/templates/default/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/default/log.config delete mode 100644 playwright/plugins/homeserver/synapse/templates/dehydration/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/dehydration/log.config delete mode 100644 playwright/plugins/homeserver/synapse/templates/email/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/email/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/email/log.config delete mode 100644 playwright/plugins/homeserver/synapse/templates/guest-enabled/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/guest-enabled/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/guest-enabled/log.config delete mode 100644 playwright/plugins/homeserver/synapse/templates/mas-oidc/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/mas-oidc/log.config delete mode 100644 playwright/plugins/mailhog/index.ts delete mode 100644 playwright/plugins/matrix-authentication-service/config.yaml delete mode 100644 playwright/plugins/matrix-authentication-service/index.ts delete mode 100644 playwright/plugins/postgres/index.ts delete mode 100644 playwright/plugins/sliding-sync-proxy/index.ts create mode 100644 playwright/services.ts create mode 100644 playwright/testcontainers/dendrite.ts create mode 100644 playwright/testcontainers/mas.ts create mode 100644 playwright/testcontainers/synapse.ts diff --git a/package.json b/package.json index b36807f7c3f..846a8ca6d9b 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,7 @@ "@sentry/webpack-plugin": "^2.7.1", "@stylistic/eslint-plugin": "^2.9.0", "@svgr/webpack": "^8.0.0", + "@testcontainers/postgresql": "^10.16.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", @@ -281,6 +282,7 @@ "stylelint-scss": "^6.0.0", "stylelint-value-no-unknown-custom-properties": "^6.0.1", "terser-webpack-plugin": "^5.3.9", + "testcontainers": "^10.16.0", "ts-node": "^10.9.1", "ts-prune": "^0.10.3", "typescript": "5.7.2", diff --git a/playwright/e2e/app-loading/guest-registration.spec.ts b/playwright/e2e/app-loading/guest-registration.spec.ts index 4455baed230..8e96a937f0f 100644 --- a/playwright/e2e/app-loading/guest-registration.spec.ts +++ b/playwright/e2e/app-loading/guest-registration.spec.ts @@ -13,11 +13,13 @@ Please see LICENSE files in the repository root for full details. import { expect, test } from "../../element-web-test"; test.use({ - startHomeserverOpts: "guest-enabled", + synapseConfigOptions: { + allow_guest_access: true, + }, config: async ({ homeserver }, use) => { await use({ default_server_config: { - "m.homeserver": { base_url: homeserver.config.baseUrl }, + "m.homeserver": { base_url: homeserver.baseUrl }, }, }); }, diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index 40c7dc0ac6c..8826cb4595c 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -23,19 +23,19 @@ async function expectBackupVersionToBe(page: Page, version: string) { masTest.describe("Encryption state after registration", () => { masTest.skip(isDendrite, "does not yet support MAS"); - masTest("Key backup is enabled by default", async ({ page, mailhog, app }) => { + masTest("Key backup is enabled by default", async ({ page, mailhogClient, app }) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); await app.settings.openUserSettings("Security & Privacy"); expect(page.getByText("This session is backing up your keys.")).toBeVisible(); }); - masTest("user is prompted to set up recovery", async ({ page, mailhog, app }) => { + masTest("user is prompted to set up recovery", async ({ page, mailhogClient, app }) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); await page.getByRole("button", { name: "Add room" }).click(); await page.getByRole("menuitem", { name: "New room" }).click(); diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 39629c82622..58bd5847b44 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -13,14 +13,16 @@ import { viewRoomSummaryByName } from "../right-panel/utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; const test = base.extend({ - // eslint-disable-next-line no-empty-pattern - startHomeserverOpts: async ({}, use) => { - await use("dehydration"); + synapseConfigOptions: { + experimental_features: { + msc2697_enabled: false, + msc3814_enabled: true, + }, }, config: async ({ homeserver, context }, use) => { const wellKnown = { "m.homeserver": { - base_url: homeserver.config.baseUrl, + base_url: homeserver.baseUrl, }, "org.matrix.msc3814": true, }; diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 337ff3d6344..3771fd73a78 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -148,7 +148,7 @@ export async function logIntoElement( // select homeserver await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index 0a12514d9ec..b307637acd1 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -8,6 +8,8 @@ Please see LICENSE files in the repository root for full details. import { expect, test } from "../../element-web-test"; import { selectHomeserver } from "../utils"; +import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const username = "user1234"; // this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. @@ -15,16 +17,8 @@ const password = "oETo7MPf0o"; const email = "user@nowhere.dummy"; test.describe("Forgot Password", () => { - test.use({ - startHomeserverOpts: ({ mailhog }, use) => - use({ - template: "email", - variables: { - SMTP_HOST: "host.containers.internal", - SMTP_PORT: mailhog.instance.smtpPort, - }, - }), - }); + test.skip(isDendrite, "not yet wired up"); + test.use(emailHomeserver); test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { await page.goto("/"); @@ -32,7 +26,7 @@ test.describe("Forgot Password", () => { await page.getByRole("link", { name: "Sign in" }).click(); // need to select a homeserver at this stage, before entering the forgot password flow - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("button", { name: "Forgot password?" }).click(); @@ -47,7 +41,7 @@ test.describe("Forgot Password", () => { await page.goto("/"); await page.getByRole("link", { name: "Sign in" }).click(); - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("button", { name: "Forgot password?" }).click(); diff --git a/playwright/e2e/login/consent.spec.ts b/playwright/e2e/login/consent.spec.ts index 4d8dd821e0c..8fd58379570 100644 --- a/playwright/e2e/login/consent.spec.ts +++ b/playwright/e2e/login/consent.spec.ts @@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import { test, expect } from "../../element-web-test"; +import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts"; test.describe("Consent", () => { + test.use(consentHomeserver); test.use({ - startHomeserverOpts: "consent", displayName: "Bob", }); diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login.spec.ts index e1307f7402d..ed0ad6c1b54 100644 --- a/playwright/e2e/login/login.spec.ts +++ b/playwright/e2e/login/login.spec.ts @@ -13,6 +13,8 @@ import { doTokenRegistration } from "./utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; import { selectHomeserver } from "../utils"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; +import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts"; +import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts"; const username = "user1234"; const password = "p4s5W0rD"; @@ -70,7 +72,7 @@ const DEVICE_SIGNING_KEYS_BODY = { async function login(page: Page, homeserver: HomeserverInstance) { await page.getByRole("link", { name: "Sign in" }).click(); - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("textbox", { name: "Username" }).fill(username); await page.getByPlaceholder("Password").fill(password); @@ -79,7 +81,7 @@ async function login(page: Page, homeserver: HomeserverInstance) { test.describe("Login", () => { test.describe("Password login", () => { - test.use({ startHomeserverOpts: "consent" }); + test.use(consentHomeserver); let creds: Credentials; @@ -101,7 +103,7 @@ test.describe("Login", () => { await page.getByRole("link", { name: "Sign in" }).click(); // first pick the homeserver, as otherwise the user picker won't be visible - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("button", { name: "Edit" }).click(); @@ -114,7 +116,7 @@ test.describe("Login", () => { await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid"); // switch back to the custom homeserver - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688 @@ -142,10 +144,10 @@ test.describe("Login", () => { homeserver, request, }) => { - const res = await request.post( - `${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, - { headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY }, - ); + const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { + headers: { Authorization: `Bearer ${creds.accessToken}` }, + data: DEVICE_SIGNING_KEYS_BODY, + }); if (res.status() / 100 !== 2) { console.log("Uploading dummy keys failed", await res.json()); } @@ -172,7 +174,7 @@ test.describe("Login", () => { request, }) => { const res = await request.post( - `${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, + `${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY }, ); if (res.status() / 100 !== 2) { @@ -203,7 +205,7 @@ test.describe("Login", () => { }) => { console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY); const res = await request.post( - `${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, + `${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY }, ); if (res.status() / 100 !== 2) { @@ -226,14 +228,7 @@ test.describe("Login", () => { // tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server test.describe("SSO login", () => { test.skip(isDendrite, "does not yet support SSO"); - - test.use({ - startHomeserverOpts: ({ oAuthServer }, use) => - use({ - template: "default", - oAuthServerPort: oAuthServer.port, - }), - }); + test.use(legacyOAuthHomeserver); test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => { // If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to @@ -247,7 +242,7 @@ test.describe("Login", () => { }); test.describe("logout", () => { - test.use({ startHomeserverOpts: "consent" }); + test.use(consentHomeserver); test("should go to login page on logout", async ({ page, user }) => { await page.getByRole("button", { name: "User menu" }).click(); @@ -262,8 +257,8 @@ test.describe("Login", () => { }); test.describe("logout with logout_redirect_url", () => { + test.use(consentHomeserver); test.use({ - startHomeserverOpts: "consent", config: { // We redirect to decoder-ring because it's a predictable page that isn't Element itself. // We could use example.org, matrix.org, or something else, however this puts dependency of external diff --git a/playwright/e2e/login/overwrite_login.spec.ts b/playwright/e2e/login/overwrite_login.spec.ts index 6d06bbc4290..6f82c30331c 100644 --- a/playwright/e2e/login/overwrite_login.spec.ts +++ b/playwright/e2e/login/overwrite_login.spec.ts @@ -24,7 +24,7 @@ test.describe("Overwrite login action", () => { expect(credentials.userId).not.toBe(bobRegister.userId); const clientCredentials /* IMatrixClientCreds */ = { - homeserverUrl: homeserver.config.baseUrl, + homeserverUrl: homeserver.baseUrl, ...bobRegister, }; diff --git a/playwright/e2e/login/soft_logout.spec.ts b/playwright/e2e/login/soft_logout.spec.ts index ca0c11132a3..777fbbd0aea 100644 --- a/playwright/e2e/login/soft_logout.spec.ts +++ b/playwright/e2e/login/soft_logout.spec.ts @@ -11,16 +11,11 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { doTokenRegistration } from "./utils"; import { Credentials } from "../../plugins/homeserver"; -import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts"; test.describe("Soft logout", () => { test.use({ displayName: "Alice", - startHomeserverOpts: ({ oAuthServer }, use) => - use({ - template: "default", - oAuthServerPort: oAuthServer.port, - }), }); test.describe("with password user", () => { @@ -47,8 +42,7 @@ test.describe("Soft logout", () => { }); test.describe("with SSO user", () => { - test.skip(isDendrite, "does not yet support SSO"); - + test.use(legacyOAuthHomeserver); test.use({ user: async ({ page, homeserver }, use) => { const user = await doTokenRegistration(page, homeserver); diff --git a/playwright/e2e/login/utils.ts b/playwright/e2e/login/utils.ts index dc856d586fe..ae794bd9119 100644 --- a/playwright/e2e/login/utils.ts +++ b/playwright/e2e/login/utils.ts @@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Page, expect } from "@playwright/test"; +import { Page, expect, Fixtures } from "@playwright/test"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; +import { Services } from "../../services.ts"; /** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element */ @@ -19,7 +20,7 @@ export async function doTokenRegistration( await page.goto("/#/login"); await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue" }).click(); // wait for the dialog to go away await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 9403406d80c..0ed405df143 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -10,41 +10,163 @@ import { API, Messages } from "mailhog"; import { Page } from "@playwright/test"; import { test as base, expect } from "../../element-web-test"; -import { MatrixAuthenticationService } from "../../plugins/matrix-authentication-service"; -import { StartHomeserverOpts } from "../../plugins/homeserver"; -export const test = base.extend<{ - masPrepare: MatrixAuthenticationService; - mas: MatrixAuthenticationService; -}>({ - // There's a bit of a chicken and egg problem between MAS & Synapse where they each need to know how to reach each other - // so spinning up a MAS is split into the prepare & start stage: prepare mas -> homeserver -> start mas to disentangle this. - masPrepare: async ({ context }, use) => { - const mas = new MatrixAuthenticationService(context); - await mas.prepare(); - await use(mas); - }, - mas: [ - async ({ masPrepare: mas, homeserver, mailhog }, use, testInfo) => { - await mas.start(homeserver, mailhog.instance); - await use(mas); - await mas.stop(testInfo); - }, - { auto: true }, - ], - startHomeserverOpts: async ({ masPrepare }, use) => { +export const test = base.extend<{}>({ + synapseConfigOptions: async ({ mas }, use) => { await use({ - template: "mas-oidc", - variables: { - MAS_PORT: masPrepare.port, + enable_registration: undefined, + enable_registration_without_verification: undefined, + disable_msisdn_registration: undefined, + experimental_features: { + msc3861: { + enabled: true, + issuer: "http://mas:8080/", + issuer_metadata: { + "issuer": `http://localhost:${mas.getMappedPort(8080)}/`, + "authorization_endpoint": "http://mas:8080/authorize", + "token_endpoint": "http://mas:8080/oauth2/token", + "jwks_uri": "http://mas:8080/oauth2/keys.json", + "registration_endpoint": "http://mas:8080/oauth2/registration", + "scopes_supported": ["openid", "email"], + "response_types_supported": ["code", "id_token", "code id_token"], + "response_modes_supported": ["form_post", "query", "fragment"], + "grant_types_supported": [ + "authorization_code", + "refresh_token", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "revocation_endpoint": "http://mas:8080/oauth2/revoke", + "revocation_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "revocation_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "introspection_endpoint": "http://mas:8080/oauth2/introspect", + "introspection_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "introspection_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "code_challenge_methods_supported": ["plain", "S256"], + "userinfo_endpoint": "http://mas:8080/oauth2/userinfo", + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "ES256K", + ], + "userinfo_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "ES256K", + ], + "display_values_supported": ["page"], + "claim_types_supported": ["normal"], + "claims_supported": [ + "iss", + "sub", + "aud", + "iat", + "exp", + "nonce", + "auth_time", + "at_hash", + "c_hash", + ], + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": false, + "prompt_values_supported": ["none", "login", "create"], + "device_authorization_endpoint": "http://mas:8080/oauth2/device", + "org.matrix.matrix-authentication-service.graphql_endpoint": "http://mas:8080/graphql", + "account_management_uri": "http://mas:8080/account/", + "account_management_actions_supported": [ + "org.matrix.profile", + "org.matrix.sessions_list", + "org.matrix.session_view", + "org.matrix.session_end", + ], + }, + client_id: "0000000000000000000SYNAPSE", + client_auth_method: "client_secret_basic", + client_secret: "SomeRandomSecret", + admin_token: "AnotherRandomSecret", + account_management_url: `http://localhost:${mas.getMappedPort(8080)}/account`, + }, }, }); }, - config: async ({ homeserver, startHomeserverOpts, context }, use) => { - const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`; + config: async ({ homeserver, mas, context }, use) => { + const issuer = `http://localhost:${mas.getMappedPort(8080)}/`; const wellKnown = { "m.homeserver": { - base_url: homeserver.config.baseUrl, + base_url: homeserver.baseUrl, }, "org.matrix.msc2965.authentication": { issuer, diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index b523f37f3ba..78696d75ef3 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -7,22 +7,22 @@ Please see LICENSE files in the repository root for full details. */ import { test, expect, registerAccountMas } from "."; -import { isDendrite } from "../../plugins/homeserver/dendrite"; import { ElementAppPage } from "../../pages/ElementAppPage.ts"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { test.skip(isDendrite, "does not yet support MAS"); test.slow(); // trace recording takes a while here - test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhog, mas }) => { - const tokenUri = `http://localhost:${mas.port}/oauth2/token`; + test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => { + const tokenUri = `http://localhost:${mas.getMappedPort(8080)}/oauth2/token`; const tokenApiPromise = page.waitForRequest( (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", ); await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); // Eventually, we should end up at the home screen. await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); @@ -49,7 +49,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { await newPage.close(); // Assert logging out revokes both tokens - const revokeUri = `http://localhost:${mas.port}/oauth2/revoke`; + const revokeUri = `http://localhost:${mas.getMappedPort(8080)}/oauth2/revoke`; const revokeAccessTokenPromise = page.waitForRequest( (request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token", ); diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 665e20ef01f..0c42b7062d9 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -7,25 +7,18 @@ Please see LICENSE files in the repository root for full details. */ import { test, expect } from "../../element-web-test"; +import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts"; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Email Registration", async () => { test.skip(isDendrite, "not yet wired up"); - + test.use(emailHomeserver); test.use({ - startHomeserverOpts: ({ mailhog }, use) => - use({ - template: "email", - variables: { - SMTP_HOST: "host.containers.internal", - SMTP_PORT: mailhog.instance.smtpPort, - }, - }), config: ({ homeserver }, use) => use({ default_server_config: { "m.homeserver": { - base_url: homeserver.config.baseUrl, + base_url: homeserver.baseUrl, }, "m.identity_server": { base_url: "https://server.invalid", @@ -41,7 +34,7 @@ test.describe("Email Registration", async () => { test( "registers an account and lands on the use case selection screen", { tag: "@screenshot" }, - async ({ page, mailhog, request, checkA11y }) => { + async ({ page, mailhogClient, request, checkA11y }) => { await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); // Hide the server text as it contains the randomly allocated Homeserver port const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; @@ -58,7 +51,7 @@ test.describe("Email Registration", async () => { await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); - const messages = await mailhog.api.messages(); + const messages = await mailhogClient.messages(); expect(messages.items).toHaveLength(1); expect(messages.items[0].to).toEqual("alice@email.com"); const [emailLink] = messages.items[0].text.match(/http.+/); diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index c1274362668..63fe24dffd0 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -7,11 +7,10 @@ Please see LICENSE files in the repository root for full details. */ import { test, expect } from "../../element-web-test"; +import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts"; test.describe("Registration", () => { - test.use({ - startHomeserverOpts: "consent", - }); + test.use(consentHomeserver); test.beforeEach(async ({ page }) => { await page.goto("/#/register"); @@ -27,7 +26,7 @@ test.describe("Registration", () => { await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); await checkA11y(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away await expect(page.getByRole("dialog")).not.toBeVisible(); @@ -88,7 +87,7 @@ test.describe("Registration", () => { test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => { await page.getByRole("button", { name: "Edit", exact: true }).click(); await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away await expect(page.getByRole("dialog")).not.toBeVisible(); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index f812fe7aecc..d105904a483 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -7,21 +7,29 @@ Please see LICENSE files in the repository root for full details. */ import { Page, Request } from "@playwright/test"; +import { GenericContainer, StartedTestContainer } from "testcontainers"; import { test as base, expect } from "../../element-web-test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; import type { Bot } from "../../pages/bot"; -import { ProxyInstance, SlidingSyncProxy } from "../../plugins/sliding-sync-proxy"; const test = base.extend<{ - slidingSyncProxy: ProxyInstance; + slidingSyncProxy: StartedTestContainer; testRoom: { roomId: string; name: string }; joinedBot: Bot; }>({ - slidingSyncProxy: async ({ context, page, homeserver }, use) => { - const proxy = new SlidingSyncProxy(homeserver.config.dockerUrl, context); - const proxyInstance = await proxy.start(); - const proxyAddress = `http://localhost:${proxyInstance.port}`; + slidingSyncProxy: async ({ network, postgres, page, homeserver }, use, testInfo) => { + const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3") + .withNetwork(network) + .withExposedPorts(8008) + .withEnvironment({ + SYNCV3_SECRET: "bwahahaha", + SYNCV3_DB: `user=postgres dbname=postgres password=${postgres.getPassword()} host=${postgres.getHost()} sslmode=disable`, + SYNCV3_SERVER: `http://${homeserver.getNetworkNames()[0]}:8008`, + }) + .start(); + + const proxyAddress = `http://localhost:${container.getMappedPort(8008)}`; await page.addInitScript((proxyAddress) => { window.localStorage.setItem( "mx_local_settings", @@ -31,8 +39,8 @@ const test = base.extend<{ ); window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true"); }, proxyAddress); - await use(proxyInstance); - await proxy.stop(); + await use(container); + await container.stop(); }, // Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script credentials: async ({ slidingSyncProxy, credentials }, use) => { diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 8206d766098..e53ebc0818c 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -6,24 +6,20 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { test as base, expect as baseExpect, Locator, Page, ExpectMatcherState, ElementHandle } from "@playwright/test"; +import { expect as baseExpect, Locator, Page, ExpectMatcherState, ElementHandle } from "@playwright/test"; import { sanitizeForFilePath } from "playwright-core/lib/utils"; import AxeBuilder from "@axe-core/playwright"; import _ from "lodash"; -import { basename, extname } from "node:path"; +import { extname } from "node:path"; -import type mailhog from "mailhog"; import type { IConfigOptions } from "../src/IConfigOptions"; -import { Credentials, Homeserver, HomeserverInstance, StartHomeserverOpts } from "./plugins/homeserver"; -import { Synapse } from "./plugins/homeserver/synapse"; -import { Dendrite, Pinecone } from "./plugins/homeserver/dendrite"; -import { Instance, MailHogServer } from "./plugins/mailhog"; +import { Credentials } from "./plugins/homeserver"; import { ElementAppPage } from "./pages/ElementAppPage"; -import { OAuthServer } from "./plugins/oauth_server"; import { Crypto } from "./pages/crypto"; import { Toasts } from "./pages/toasts"; import { Bot, CreateBotOpts } from "./pages/bot"; import { Webserver } from "./plugins/webserver"; +import { test as base } from "./services.ts"; // Enable experimental service worker support // See https://playwright.dev/docs/service-workers-experimental#how-to-enable @@ -68,14 +64,6 @@ export interface Fixtures { */ config: typeof CONFIG_JSON; - /** - * The options with which to run the {@link #homeserver} fixture. - */ - startHomeserverOpts: StartHomeserverOpts | string; - - homeserver: HomeserverInstance; - oAuthServer: { port: number }; - /** * The displayname to use for the user registered in {@link #credentials}. * @@ -113,7 +101,6 @@ export interface Fixtures { */ app: ElementAppPage; - mailhog: { api: mailhog.API; instance: Instance }; crypto: Crypto; room?: { roomId: string }; toasts: Toasts; @@ -150,45 +137,6 @@ export const test = base.extend({ await use(page); }, - startHomeserverOpts: "default", - homeserver: async ({ request, startHomeserverOpts: opts }, use, testInfo) => { - if (typeof opts === "string") { - opts = { template: opts }; - } - - let server: Homeserver; - const homeserverName = process.env["PLAYWRIGHT_HOMESERVER"]; - switch (homeserverName) { - case "dendrite": - server = new Dendrite(request); - break; - case "pinecone": - server = new Pinecone(request); - break; - default: - server = new Synapse(request); - } - - await use(await server.start(opts)); - const logs = await server.stop(); - - if (testInfo.status !== "passed") { - for (const path of logs) { - await testInfo.attach(`homeserver-${basename(path)}`, { - path, - contentType: "text/plain", - }); - } - } - }, - // eslint-disable-next-line no-empty-pattern - oAuthServer: async ({}, use) => { - const server = new OAuthServer(); - const port = server.start(); - await use({ port }); - server.stop(); - }, - displayName: undefined, credentials: async ({ homeserver, displayName: testDisplayName }, use) => { const names = ["Alice", "Bob", "Charlie", "Daniel", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Judy"]; @@ -220,7 +168,7 @@ export const test = base.extend({ // Ensure the language is set to a consistent value window.localStorage.setItem("mx_local_settings", '{"language":"en"}'); }, - { baseUrl: homeserver.config.baseUrl, credentials }, + { baseUrl: homeserver.baseUrl, credentials }, ); await use(page); }, @@ -265,14 +213,6 @@ export const test = base.extend({ await use(bot); }, - // eslint-disable-next-line no-empty-pattern - mailhog: async ({}, use) => { - const mailhog = new MailHogServer(); - const instance = await mailhog.start(); - await use(instance); - await mailhog.stop(); - }, - // eslint-disable-next-line no-empty-pattern webserver: async ({}, use) => { const webserver = new Webserver(); diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index d50a0e84ee1..6b5b2a326c9 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -97,7 +97,7 @@ export class Bot extends Client { private async buildClient(): Promise> { const credentials = await this.getCredentials(); const clientHandle = await this.page.evaluateHandle( - async ({ homeserver, credentials, opts }) => { + async ({ baseUrl, credentials, opts }) => { function getLogger(loggerName: string): Logger { const logger = { getChild: (namespace: string) => getLogger(`${loggerName}:${namespace}`), @@ -157,7 +157,7 @@ export class Bot extends Client { }; const cli = new window.matrixcs.MatrixClient({ - baseUrl: homeserver.baseUrl, + baseUrl, userId: credentials.userId, deviceId: credentials.deviceId, accessToken: credentials.accessToken, @@ -179,7 +179,7 @@ export class Bot extends Client { return cli; }, { - homeserver: this.homeserver.config, + baseUrl: this.homeserver.baseUrl, credentials, opts: this.opts, }, diff --git a/playwright/pages/crypto.ts b/playwright/pages/crypto.ts index f221412a7ce..138dfa1c007 100644 --- a/playwright/pages/crypto.ts +++ b/playwright/pages/crypto.ts @@ -27,7 +27,7 @@ export class Crypto { accessToken: window.mxMatrixClientPeg.get().getAccessToken(), })); - const res = await this.request.post(`${this.homeserver.config.baseUrl}/_matrix/client/v3/keys/query`, { + const res = await this.request.post(`${this.homeserver.baseUrl}/_matrix/client/v3/keys/query`, { headers: { Authorization: `Bearer ${accessToken}` }, data: { device_keys: { [userId]: [] } }, }); diff --git a/playwright/plugins/docker/index.ts b/playwright/plugins/docker/index.ts deleted file mode 100644 index 6cc13860be3..00000000000 --- a/playwright/plugins/docker/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as os from "os"; -import * as crypto from "crypto"; -import * as childProcess from "child_process"; -import * as fse from "fs-extra"; - -/** - * @param cmd - command to execute - * @param args - arguments to pass to executed command - * @param suppressOutput - whether to suppress the stdout and stderr resulting from this command. - * @return Promise which resolves to an object containing the string value of what was - * written to stdout and stderr by the executed command. - */ -const exec = (cmd: string, args: string[], suppressOutput = false): Promise<{ stdout: string; stderr: string }> => { - return new Promise((resolve, reject) => { - if (!suppressOutput) { - const log = ["Running command:", cmd, ...args, "\n"].join(" "); - // When in CI mode we combine reports from multiple runners into a single HTML report - // which has separate files for stdout and stderr, so we print the executed command to both - process.stdout.write(log); - if (process.env.CI) process.stderr.write(log); - } - const { stdout, stderr } = childProcess.execFile(cmd, args, { encoding: "utf8" }, (err, stdout, stderr) => { - if (err) reject(err); - resolve({ stdout, stderr }); - if (!suppressOutput) { - process.stdout.write("\n"); - if (process.env.CI) process.stderr.write("\n"); - } - }); - if (!suppressOutput) { - stdout.pipe(process.stdout); - stderr.pipe(process.stderr); - } - }); -}; - -export class Docker { - public id: string; - - async run(opts: { image: string; containerName: string; params?: string[]; cmd?: string[] }): Promise { - const userInfo = os.userInfo(); - const params = opts.params ?? []; - - const isPodman = await Docker.isPodman(); - if (params.includes("-v") && userInfo.uid >= 0) { - // Run the docker container as our uid:gid to prevent problems with permissions. - if (isPodman) { - // Note: this setup is for podman rootless containers. - - // In podman, run as root in the container, which maps to the current - // user on the host. This is probably the default since Synapse's - // Dockerfile doesn't specify, but we're being explicit here - // because it's important for the permissions to work. - params.push("-u", "0:0"); - - // Tell Synapse not to switch UID - params.push("-e", "UID=0"); - params.push("-e", "GID=0"); - } else { - params.push("-u", `${userInfo.uid}:${userInfo.gid}`); - } - } - - // Make host.containers.internal work to allow the container to talk to other services via host ports. - if (isPodman) { - params.push("--network"); - params.push("slirp4netns:allow_host_loopback=true"); - } else { - // Docker for Desktop includes a host-gateway mapping on host.docker.internal but to simplify the config - // we use the Podman variant host.containers.internal in all environments. - params.push("--add-host"); - params.push("host.containers.internal:host-gateway"); - } - - // Provided we are not running in CI, add a `--rm` parameter. - // There is no need to remove containers in CI (since they are automatically removed anyway), and - // `--rm` means that if a container crashes this means its logs are wiped out. - if (!process.env.CI) params.unshift("--rm"); - - const args = [ - "run", - "--name", - `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`, - "-d", - ...params, - opts.image, - ]; - - if (opts.cmd) args.push(...opts.cmd); - - const { stdout } = await exec("docker", args); - this.id = stdout.trim(); - return this.id; - } - - async stop(): Promise { - try { - await exec("docker", ["stop", this.id]); - } catch (err) { - console.error(`Failed to stop docker container`, this.id, err); - } - } - - /** - * @param params - list of parameters to pass to `docker exec` - * @param suppressOutput - whether to suppress the stdout and stderr resulting from this command. - */ - async exec(params: string[], suppressOutput = true): Promise { - await exec("docker", ["exec", this.id, ...params], suppressOutput); - } - - async getContainerIp(): Promise { - const { stdout } = await exec("docker", ["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", this.id]); - return stdout.trim(); - } - - async persistLogsToFile(args: { stdoutFile?: string; stderrFile?: string }): Promise { - const stdoutFile = args.stdoutFile ? await fse.open(args.stdoutFile, "w") : "ignore"; - const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore"; - await new Promise((resolve) => { - childProcess - .spawn("docker", ["logs", this.id], { - stdio: ["ignore", stdoutFile, stderrFile], - }) - .once("close", resolve); - }); - if (args.stdoutFile) await fse.close(stdoutFile); - if (args.stderrFile) await fse.close(stderrFile); - } - - /** - * Detects whether the docker command is actually podman. - * To do this, it looks for "podman" in the output of "docker --help". - */ - static _isPodman?: boolean; - static async isPodman(): Promise { - if (Docker._isPodman === undefined) { - const { stdout } = await exec("docker", ["--help"], true); - Docker._isPodman = stdout.toLowerCase().includes("podman"); - } - return Docker._isPodman; - } -} diff --git a/playwright/plugins/homeserver/dendrite/index.ts b/playwright/plugins/homeserver/dendrite/index.ts index 0886dc1586d..9bb16746496 100644 --- a/playwright/plugins/homeserver/dendrite/index.ts +++ b/playwright/plugins/homeserver/dendrite/index.ts @@ -6,142 +6,39 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import * as path from "node:path"; -import * as os from "node:os"; -import * as fse from "fs-extra"; - -import { getFreePort } from "../../utils/port"; -import { Homeserver, HomeserverConfig, HomeserverInstance, StartHomeserverOpts } from "../"; -import { randB64Bytes } from "../../utils/rand"; -import { Synapse } from "../synapse"; -import { Docker } from "../../docker"; - -const dockerConfigDir = "/etc/dendrite/"; -const dendriteConfigFile = "dendrite.yaml"; - -// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it -export class Dendrite extends Synapse implements Homeserver, HomeserverInstance { - protected image = "matrixdotorg/dendrite-monolith:main"; - protected entrypoint = "/usr/bin/dendrite"; - - /** - * Start a dendrite instance: the template must be the name of one of the templates - * in the playwright/plugins/dendritedocker/templates directory - * @param opts - */ - public async start(opts: StartHomeserverOpts): Promise { - const denCfg = await cfgDirFromTemplate(this.image, opts); - - console.log(`Starting dendrite with config dir ${denCfg.configDir}...`); - - const dendriteId = await this.docker.run({ - image: this.image, - params: [ - "-v", - `${denCfg.configDir}:` + dockerConfigDir, - "-p", - `${denCfg.port}:8008/tcp`, - "--entrypoint", - this.entrypoint, - ], - containerName: `react-sdk-playwright-dendrite`, - cmd: ["--config", dockerConfigDir + dendriteConfigFile, "--really-enable-open-registration", "true", "run"], - }); - - console.log(`Started dendrite with id ${dendriteId} on port ${denCfg.port}.`); - - // Await Dendrite healthcheck - await this.docker.exec([ - "curl", - "--connect-timeout", - "30", - "--retry", - "30", - "--retry-delay", - "1", - "--retry-all-errors", - "--silent", - "http://localhost:8008/_matrix/client/versions", - ]); - - const dockerUrl = `http://${await this.docker.getContainerIp()}:8008`; - this.config = { - ...denCfg, - serverId: dendriteId, - dockerUrl, - }; - return this; - } - - public async stop(): Promise { - if (!this.config) throw new Error("Missing existing dendrite instance, did you call stop() before start()?"); - - const dendriteLogsPath = path.join("playwright", "dendritelogs", this.config.serverId); - await fse.ensureDir(dendriteLogsPath); - - await this.docker.persistLogsToFile({ - stdoutFile: path.join(dendriteLogsPath, "stdout.log"), - stderrFile: path.join(dendriteLogsPath, "stderr.log"), - }); - - await this.docker.stop(); - - await fse.remove(this.config.configDir); - - console.log(`Stopped dendrite id ${this.config.serverId}.`); - - return [path.join(dendriteLogsPath, "stdout.log"), path.join(dendriteLogsPath, "stderr.log")]; - } -} - -export class Pinecone extends Dendrite { - protected image = "matrixdotorg/dendrite-demo-pinecone:main"; - protected entrypoint = "/usr/bin/dendrite-demo-pinecone"; -} - -async function cfgDirFromTemplate( - dendriteImage: string, - opts: StartHomeserverOpts, -): Promise> { - const template = "default"; // XXX: for now we only have one template - const templateDir = path.join(__dirname, "templates", template); - - const stats = await fse.stat(templateDir); - if (!stats?.isDirectory) { - throw new Error(`No such template: ${template}`); - } - const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-dendritedocker-")); - - // copy the contents of the template dir, omitting homeserver.yaml as we'll template that - console.log(`Copy ${templateDir} -> ${tempDir}`); - await fse.copy(templateDir, tempDir, { filter: (f) => path.basename(f) !== dendriteConfigFile }); - - const registrationSecret = randB64Bytes(16); - - const port = await getFreePort(); - const baseUrl = `http://localhost:${port}`; - - // now copy homeserver.yaml, applying substitutions - console.log(`Gen ${path.join(templateDir, dendriteConfigFile)}`); - let hsYaml = await fse.readFile(path.join(templateDir, dendriteConfigFile), "utf8"); - hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret); - await fse.writeFile(path.join(tempDir, dendriteConfigFile), hsYaml); - - const docker = new Docker(); - await docker.run({ - image: dendriteImage, - params: ["--entrypoint=", "-v", `${tempDir}:/mnt`], - containerName: `react-sdk-playwright-dendrite-keygen`, - cmd: ["/usr/bin/generate-keys", "-private-key", "/mnt/matrix_key.pem"], - }); - - return { - port, - baseUrl, - configDir: tempDir, - registrationSecret, - }; -} +// export const dendriteHomeserver: Fixtures & Fixtures = { +// _homeserver: async ({ request }, use) => { +// const container = new SynapseContainer(request); +// await use(container); +// +// container.withConfig({ +// oidc_providers: [ +// { +// idp_id: "test", +// idp_name: "OAuth test", +// issuer: `http://localhost:${port}/oauth`, +// authorization_endpoint: `http://localhost:${port}/oauth/auth.html`, +// // the token endpoint receives requests from synapse, +// // rather than the webapp, so needs to escape the docker container. +// token_endpoint: `http://host.testcontainers.internal:${port}/oauth/token`, +// userinfo_endpoint: `http://host.testcontainers.internal:${port}/oauth/userinfo`, +// client_id: "synapse", +// discover: false, +// scopes: ["profile"], +// skip_verification: true, +// client_auth_method: "none", +// user_mapping_provider: { +// config: { +// display_name_template: "{{ user.name }}", +// }, +// }, +// }, +// ], +// }); +// await use(container); +// server.stop(); +// }, +// }; export function isDendrite(): boolean { return process.env["PLAYWRIGHT_HOMESERVER"] === "dendrite" || process.env["PLAYWRIGHT_HOMESERVER"] === "pinecone"; diff --git a/playwright/plugins/homeserver/dendrite/templates/default/dendrite.yaml b/playwright/plugins/homeserver/dendrite/templates/default/dendrite.yaml deleted file mode 100644 index 634cebbc876..00000000000 --- a/playwright/plugins/homeserver/dendrite/templates/default/dendrite.yaml +++ /dev/null @@ -1,378 +0,0 @@ -# This is the Dendrite configuration file. -# -# The configuration is split up into sections - each Dendrite component has a -# configuration section, in addition to the "global" section which applies to -# all components. - -# The version of the configuration file. -version: 2 - -# Global Matrix configuration. This configuration applies to all components. -global: - # The domain name of this homeserver. - server_name: localhost - - # The path to the signing private key file, used to sign requests and events. - # Note that this is NOT the same private key as used for TLS! To generate a - # signing key, use "./bin/generate-keys --private-key matrix_key.pem". - private_key: matrix_key.pem - - # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision) - # to old signing keys that were formerly in use on this domain name. These - # keys will not be used for federation request or event signing, but will be - # provided to any other homeserver that asks when trying to verify old events. - old_private_keys: - # If the old private key file is available: - # - private_key: old_matrix_key.pem - # expired_at: 1601024554498 - # If only the public key (in base64 format) and key ID are known: - # - public_key: mn59Kxfdq9VziYHSBzI7+EDPDcBS2Xl7jeUdiiQcOnM= - # key_id: ed25519:mykeyid - # expired_at: 1601024554498 - - # How long a remote server can cache our server signing key before requesting it - # again. Increasing this number will reduce the number of requests made by other - # servers for our key but increases the period that a compromised key will be - # considered valid by other homeservers. - key_validity_period: 168h0m0s - - # Global database connection pool, for PostgreSQL monolith deployments only. If - # this section is populated then you can omit the "database" blocks in all other - # sections. For polylith deployments, or monolith deployments using SQLite databases, - # you must configure the "database" block for each component instead. - # database: - # connection_string: postgresql://username:password@hostname/dendrite?sslmode=disable - # max_open_conns: 90 - # max_idle_conns: 5 - # conn_max_lifetime: -1 - - # Configuration for in-memory caches. Caches can often improve performance by - # keeping frequently accessed items (like events, identifiers etc.) in memory - # rather than having to read them from the database. - cache: - # The estimated maximum size for the global cache in bytes, or in terabytes, - # gigabytes, megabytes or kilobytes when the appropriate 'tb', 'gb', 'mb' or - # 'kb' suffix is specified. Note that this is not a hard limit, nor is it a - # memory limit for the entire process. A cache that is too small may ultimately - # provide little or no benefit. - max_size_estimated: 1gb - - # The maximum amount of time that a cache entry can live for in memory before - # it will be evicted and/or refreshed from the database. Lower values result in - # easier admission of new cache entries but may also increase database load in - # comparison to higher values, so adjust conservatively. Higher values may make - # it harder for new items to make it into the cache, e.g. if new rooms suddenly - # become popular. - max_age: 1h - - # The server name to delegate server-server communications to, with optional port - # e.g. localhost:443 - well_known_server_name: "" - - # The server name to delegate client-server communications to, with optional port - # e.g. localhost:443 - well_known_client_name: "" - - # Lists of domains that the server will trust as identity servers to verify third - # party identifiers such as phone numbers and email addresses. - trusted_third_party_id_servers: - - matrix.org - - vector.im - - # Disables federation. Dendrite will not be able to communicate with other servers - # in the Matrix federation and the federation API will not be exposed. - disable_federation: false - - # Configures the handling of presence events. Inbound controls whether we receive - # presence events from other servers, outbound controls whether we send presence - # events for our local users to other servers. - presence: - enable_inbound: false - enable_outbound: false - - # Configures phone-home statistics reporting. These statistics contain the server - # name, number of active users and some information on your deployment config. - # We use this information to understand how Dendrite is being used in the wild. - report_stats: - enabled: false - endpoint: https://matrix.org/report-usage-stats/push - - # Server notices allows server admins to send messages to all users on the server. - server_notices: - enabled: false - # The local part, display name and avatar URL (as a mxc:// URL) for the user that - # will send the server notices. These are visible to all users on the deployment. - local_part: "_server" - display_name: "Server Alerts" - avatar_url: "" - # The room name to be used when sending server notices. This room name will - # appear in user clients. - room_name: "Server Alerts" - - # Configuration for NATS JetStream - jetstream: - # A list of NATS Server addresses to connect to. If none are specified, an - # internal NATS server will be started automatically when running Dendrite in - # monolith mode. For polylith deployments, it is required to specify the address - # of at least one NATS Server node. - addresses: - # - localhost:4222 - - # Disable the validation of TLS certificates of NATS. This is - # not recommended in production since it may allow NATS traffic - # to be sent to an insecure endpoint. - disable_tls_validation: false - - # Persistent directory to store JetStream streams in. This directory should be - # preserved across Dendrite restarts. - storage_path: ./ - - # The prefix to use for stream names for this homeserver - really only useful - # if you are running more than one Dendrite server on the same NATS deployment. - topic_prefix: Dendrite - - # Configuration for Prometheus metric collection. - metrics: - enabled: false - basic_auth: - username: metrics - password: metrics - - # Optional DNS cache. The DNS cache may reduce the load on DNS servers if there - # is no local caching resolver available for use. - dns_cache: - enabled: false - cache_size: 256 - cache_lifetime: "5m" # 5 minutes; https://pkg.go.dev/time@master#ParseDuration - -# Configuration for the Appservice API. -app_service_api: - # Disable the validation of TLS certificates of appservices. This is - # not recommended in production since it may allow appservice traffic - # to be sent to an insecure endpoint. - disable_tls_validation: false - - # Appservice configuration files to load into this homeserver. - config_files: - # - /path/to/appservice_registration.yaml - -# Configuration for the Client API. -client_api: - # Prevents new users from being able to register on this homeserver, except when - # using the registration shared secret below. - registration_disabled: false - - # Prevents new guest accounts from being created. Guest registration is also - # disabled implicitly by setting 'registration_disabled' above. - guests_disabled: true - - # If set, allows registration by anyone who knows the shared secret, regardless - # of whether registration is otherwise disabled. - registration_shared_secret: "{{REGISTRATION_SECRET}}" - - # Whether to require reCAPTCHA for registration. If you have enabled registration - # then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used - # for coordinated spam attacks. - enable_registration_captcha: false - - # Settings for ReCAPTCHA. - recaptcha_public_key: "" - recaptcha_private_key: "" - recaptcha_bypass_secret: "" - - # To use hcaptcha.com instead of ReCAPTCHA, set the following parameters, otherwise just keep them empty. - # recaptcha_siteverify_api: "https://hcaptcha.com/siteverify" - # recaptcha_api_js_url: "https://js.hcaptcha.com/1/api.js" - # recaptcha_form_field: "h-captcha-response" - # recaptcha_sitekey_class: "h-captcha" - - # TURN server information that this homeserver should send to clients. - turn: - turn_user_lifetime: "5m" - turn_uris: - # - turn:turn.server.org?transport=udp - # - turn:turn.server.org?transport=tcp - turn_shared_secret: "" - # If your TURN server requires static credentials, then you will need to enter - # them here instead of supplying a shared secret. Note that these credentials - # will be visible to clients! - # turn_username: "" - # turn_password: "" - - # Settings for rate-limited endpoints. Rate limiting kicks in after the threshold - # number of "slots" have been taken by requests from a specific host. Each "slot" - # will be released after the cooloff time in milliseconds. Server administrators - # and appservice users are exempt from rate limiting by default. - rate_limiting: - enabled: true - threshold: 20 - cooloff_ms: 500 - exempt_user_ids: - # - "@user:domain.com" - -# Configuration for the Federation API. -federation_api: - # How many times we will try to resend a failed transaction to a specific server. The - # backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. Once - # the max retries are exceeded, Dendrite will no longer try to send transactions to - # that server until it comes back to life and connects to us again. - send_max_retries: 16 - - # Disable the validation of TLS certificates of remote federated homeservers. Do not - # enable this option in production as it presents a security risk! - disable_tls_validation: false - - # Disable HTTP keepalives, which also prevents connection reuse. Dendrite will typically - # keep HTTP connections open to remote hosts for 5 minutes as they can be reused much - # more quickly than opening new connections each time. Disabling keepalives will close - # HTTP connections immediately after a successful request but may result in more CPU and - # memory being used on TLS handshakes for each new connection instead. - disable_http_keepalives: false - - # Perspective keyservers to use as a backup when direct key fetches fail. This may - # be required to satisfy key requests for servers that are no longer online when - # joining some rooms. - key_perspectives: - - server_name: matrix.org - keys: - - key_id: ed25519:auto - public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw - - key_id: ed25519:a_RXGa - public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ - - # This option will control whether Dendrite will prefer to look up keys directly - # or whether it should try perspective servers first, using direct fetches as a - # last resort. - prefer_direct_fetch: false - - database: - connection_string: file:dendrite-federationapi.db - -# Configuration for the Media API. -media_api: - # Storage path for uploaded media. May be relative or absolute. - base_path: ./media_store - - # The maximum allowed file size (in bytes) for media uploads to this homeserver - # (0 = unlimited). If using a reverse proxy, ensure it allows requests at least - #this large (e.g. the client_max_body_size setting in nginx). - max_file_size_bytes: 10485760 - - # Whether to dynamically generate thumbnails if needed. - dynamic_thumbnails: false - - # The maximum number of simultaneous thumbnail generators to run. - max_thumbnail_generators: 10 - - # A list of thumbnail sizes to be generated for media content. - thumbnail_sizes: - - width: 32 - height: 32 - method: crop - - width: 96 - height: 96 - method: crop - - width: 640 - height: 480 - method: scale - - database: - connection_string: file:dendrite-mediaapi.db - -# Configuration for enabling experimental MSCs on this homeserver. -mscs: - mscs: - # - msc2836 # (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836) - # - msc2946 # (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946) - - database: - connection_string: file:dendrite-msc.db - -# Configuration for the Sync API. -sync_api: - # This option controls which HTTP header to inspect to find the real remote IP - # address of the client. This is likely required if Dendrite is running behind - # a reverse proxy server. - # real_ip_header: X-Real-IP - - # Configuration for the full-text search engine. - search: - # Whether or not search is enabled. - enabled: false - - # The path where the search index will be created in. - index_path: "./searchindex" - - # The language most likely to be used on the server - used when indexing, to - # ensure the returned results match expectations. A full list of possible languages - # can be found at https://github.com/blevesearch/bleve/tree/master/analysis/lang - language: "en" - - database: - connection_string: file:dendrite-syncapi.db - -# Configuration for the User API. -user_api: - # The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31 - # See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information. - # Setting this lower makes registration/login consume less CPU resources at the cost - # of security should the database be compromised. Setting this higher makes registration/login - # consume more CPU resources but makes it harder to brute force password hashes. This value - # can be lowered if performing tests or on embedded Dendrite instances (e.g WASM builds). - bcrypt_cost: 10 - - # The length of time that a token issued for a relying party from - # /_matrix/client/r0/user/{userId}/openid/request_token endpoint - # is considered to be valid in milliseconds. - # The default lifetime is 3600000ms (60 minutes). - # openid_token_lifetime_ms: 3600000 - - # Users who register on this homeserver will automatically be joined to the rooms listed under "auto_join_rooms" option. - # By default, any room aliases included in this list will be created as a publicly joinable room - # when the first user registers for the homeserver. If the room already exists, - # make certain it is a publicly joinable room, i.e. the join rule of the room must be set to 'public'. - # As Spaces are just rooms under the hood, Space aliases may also be used. - auto_join_rooms: - # - "#main:matrix.org" - - account_database: - connection_string: file:dendrite-userapi.db - -room_server: - database: - connection_string: file:dendrite-roomserverapi.db - -key_server: - database: - connection_string: file:dendrite-keyserverapi.db - -relay_api: - database: - connection_string: file:dendrite-relayapi.db - -# Configuration for Opentracing. -# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on -# how this works and how to set it up. -tracing: - enabled: false - jaeger: - serviceName: "" - disabled: false - rpc_metrics: false - tags: [] - sampler: null - reporter: null - headers: null - baggage_restrictions: null - throttler: null - -# Logging configuration. The "std" logging type controls the logs being sent to -# stdout. The "file" logging type controls logs being written to a log folder on -# the disk. Supported log levels are "debug", "info", "warn", "error". -logging: - - type: std - level: debug - - type: file - level: debug - params: - path: ./logs diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index c17ea15f554..b6b4b5ee995 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -6,16 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -export interface HomeserverConfig { - readonly configDir: string; - readonly baseUrl: string; - readonly port: number; - readonly registrationSecret: string; - readonly dockerUrl: string; -} - export interface HomeserverInstance { - readonly config: HomeserverConfig; + readonly baseUrl: string; /** * Register a user on the given Homeserver using the shared registration secret. @@ -42,27 +34,6 @@ export interface HomeserverInstance { setThreepid(userId: string, medium: string, address: string): Promise; } -export interface StartHomeserverOpts { - /** path to template within playwright/plugins/{homeserver}docker/template/ directory. */ - template: string; - - /** Port of an OAuth server to configure the homeserver to use */ - oAuthServerPort?: number; - - /** Additional variables to inject into the configuration template **/ - variables?: Record; -} - -export interface Homeserver { - start(opts: StartHomeserverOpts): Promise; - /** - * Stop this test homeserver instance. - * - * @returns A list of paths relative to the cwd for logfiles generated during this test run. - */ - stop(): Promise; -} - export interface Credentials { accessToken: string; userId: string; diff --git a/playwright/plugins/homeserver/synapse/consentHomeserver.ts b/playwright/plugins/homeserver/synapse/consentHomeserver.ts new file mode 100644 index 00000000000..46c4c9f23dd --- /dev/null +++ b/playwright/plugins/homeserver/synapse/consentHomeserver.ts @@ -0,0 +1,56 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2023 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { Fixtures } from "@playwright/test"; + +import { Services } from "../../../services.ts"; + +export const consentHomeserver: Fixtures = { + _homeserver: async ({ _homeserver: container, mailhog }, use) => { + container + .withCopyDirectoriesToContainer([ + { source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" }, + ]) + .withConfig({ + email: { + enable_notifs: false, + smtp_host: "mailhog", + smtp_port: 1025, + smtp_user: "username", + smtp_pass: "password", + require_transport_security: false, + notif_from: "Your Friendly %(app)s homeserver ", + app_name: "Matrix", + notif_template_html: "notif_mail.html", + notif_template_text: "notif_mail.txt", + notif_for_new_users: true, + client_base_url: "http://localhost/element", + }, + user_consent: { + template_dir: "/data/res/templates/privacy", + version: "1.0", + server_notice_content: { + msgtype: "m.text", + body: "To continue using this homeserver you must review and agree to the terms and conditions at %(consent_uri)s", + }, + send_server_notice_to_guests: true, + block_events_error: + "To continue using this homeserver you must review and agree to the terms and conditions at %(consent_uri)s", + require_at_registration: true, + }, + server_notices: { + system_mxid_localpart: "notices", + system_mxid_display_name: "Server Notices", + system_mxid_avatar_url: "mxc://localhost/oumMVlgDnLYFaPVkExemNVVZ", + room_name: "Server Notices", + }, + }) + .withConfigField("listeners[0].resources[0].names", ["client", "consent"]); + await use(container); + }, +}; diff --git a/playwright/plugins/homeserver/synapse/emailHomeserver.ts b/playwright/plugins/homeserver/synapse/emailHomeserver.ts new file mode 100644 index 00000000000..07dfe69264c --- /dev/null +++ b/playwright/plugins/homeserver/synapse/emailHomeserver.ts @@ -0,0 +1,28 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2023 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { Fixtures } from "@playwright/test"; + +import { Services } from "../../../services.ts"; + +export const emailHomeserver: Fixtures = { + _homeserver: async ({ _homeserver: container, mailhog }, use) => { + container.withConfig({ + enable_registration_without_verification: undefined, + disable_msisdn_registration: undefined, + registrations_require_3pid: ["email"], + email: { + smtp_host: "mailhog", + smtp_port: 25, + notif_from: "Your Friendly %(app)s homeserver ", + app_name: "my_branded_matrix_server", + }, + }); + await use(container); + }, +}; diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts deleted file mode 100644 index c98a1f59bc5..00000000000 --- a/playwright/plugins/homeserver/synapse/index.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as path from "node:path"; -import * as os from "node:os"; -import * as crypto from "node:crypto"; -import * as fse from "fs-extra"; -import { APIRequestContext } from "@playwright/test"; - -import { getFreePort } from "../../utils/port"; -import { Docker } from "../../docker"; -import { HomeserverConfig, HomeserverInstance, Homeserver, StartHomeserverOpts, Credentials } from ".."; -import { randB64Bytes } from "../../utils/rand"; - -// Docker tag to use for synapse docker image. -// We target a specific digest as every now and then a Synapse update will break our CI. -// This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:39f94b005e87cd3042c2535c37d8d9f915a88072fe79f6283ac18977fe134321"; - -async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { - const templateDir = path.join(__dirname, "templates", opts.template); - - const stats = await fse.stat(templateDir); - if (!stats?.isDirectory) { - throw new Error(`No such template: ${opts.template}`); - } - const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-synapsedocker-")); - - // copy the contents of the template dir, omitting homeserver.yaml as we'll template that - console.log(`Copy ${templateDir} -> ${tempDir}`); - await fse.copy(templateDir, tempDir, { filter: (f) => path.basename(f) !== "homeserver.yaml" }); - - const registrationSecret = randB64Bytes(16); - const macaroonSecret = randB64Bytes(16); - const formSecret = randB64Bytes(16); - - const port = await getFreePort(); - const baseUrl = `http://localhost:${port}`; - - // now copy homeserver.yaml, applying substitutions - const templateHomeserver = path.join(templateDir, "homeserver.yaml"); - const outputHomeserver = path.join(tempDir, "homeserver.yaml"); - console.log(`Gen ${templateHomeserver} -> ${outputHomeserver}`); - let hsYaml = await fse.readFile(templateHomeserver, "utf8"); - hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret); - hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); - hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); - hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); - if (opts.oAuthServerPort) { - hsYaml = hsYaml.replace(/{{OAUTH_SERVER_PORT}}/g, opts.oAuthServerPort.toString()); - } - if (opts.variables) { - for (const key in opts.variables) { - hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), String(opts.variables[key])); - } - } - - await fse.writeFile(outputHomeserver, hsYaml); - - // now generate a signing key (we could use synapse's config generation for - // this, or we could just do this...) - // NB. This assumes the homeserver.yaml specifies the key in this location - const signingKey = randB64Bytes(32); - const outputSigningKey = path.join(tempDir, "localhost.signing.key"); - console.log(`Gen -> ${outputSigningKey}`); - await fse.writeFile(outputSigningKey, `ed25519 x ${signingKey}`); - - // Allow anyone to read, write and execute in the /temp/react-sdk-synapsedocker-xxx directory - // so that the DIND setup that we use to update the playwright screenshots work without any issues. - await fse.chmod(tempDir, 0o757); - - return { - port, - baseUrl, - configDir: tempDir, - registrationSecret, - }; -} - -export class Synapse implements Homeserver, HomeserverInstance { - protected docker: Docker = new Docker(); - public config: HomeserverConfig & { serverId: string }; - - private adminToken?: string; - - public constructor(private readonly request: APIRequestContext) {} - - /** - * Start a synapse instance: the template must be the name of - * one of the templates in the playwright/plugins/synapsedocker/templates - * directory. - */ - public async start(opts: StartHomeserverOpts): Promise { - if (this.config) await this.stop(); - - const synCfg = await cfgDirFromTemplate(opts); - console.log(`Starting synapse with config dir ${synCfg.configDir}...`); - const dockerSynapseParams = ["-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`]; - const synapseId = await this.docker.run({ - image: `ghcr.io/element-hq/synapse:${DOCKER_TAG}`, - containerName: `react-sdk-playwright-synapse`, - params: dockerSynapseParams, - cmd: ["run"], - }); - console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`); - // Await Synapse healthcheck - await this.docker.exec([ - "curl", - "--connect-timeout", - "30", - "--retry", - "30", - "--retry-delay", - "1", - "--retry-all-errors", - "--silent", - "http://localhost:8008/health", - ]); - const dockerUrl = `http://${await this.docker.getContainerIp()}:8008`; - this.config = { - ...synCfg, - serverId: synapseId, - dockerUrl, - }; - return this; - } - - public async stop(): Promise { - if (!this.config) throw new Error("Missing existing synapse instance, did you call stop() before start()?"); - const id = this.config.serverId; - const synapseLogsPath = path.join("playwright", "logs", "synapse", id); - await fse.ensureDir(synapseLogsPath); - await this.docker.persistLogsToFile({ - stdoutFile: path.join(synapseLogsPath, "stdout.log"), - stderrFile: path.join(synapseLogsPath, "stderr.log"), - }); - await this.docker.stop(); - await fse.remove(this.config.configDir); - console.log(`Stopped synapse id ${id}.`); - - return [path.join(synapseLogsPath, "stdout.log"), path.join(synapseLogsPath, "stderr.log")]; - } - - private async registerUserInternal( - username: string, - password: string, - displayName?: string, - admin = false, - ): Promise { - const url = `${this.config.baseUrl}/_synapse/admin/v1/register`; - const { nonce } = await this.request.get(url).then((r) => r.json()); - const mac = crypto - .createHmac("sha1", this.config.registrationSecret) - .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) - .digest("hex"); - const res = await this.request.post(url, { - data: { - nonce, - username, - password, - mac, - admin, - displayname: displayName, - }, - }); - - if (!res.ok()) { - throw await res.json(); - } - - const data = await res.json(); - return { - homeServer: data.home_server, - accessToken: data.access_token, - userId: data.user_id, - deviceId: data.device_id, - password, - displayName, - }; - } - - public registerUser(username: string, password: string, displayName?: string): Promise { - return this.registerUserInternal(username, password, displayName, false); - } - - public async loginUser(userId: string, password: string): Promise { - const url = `${this.config.baseUrl}/_matrix/client/v3/login`; - const res = await this.request.post(url, { - data: { - type: "m.login.password", - identifier: { - type: "m.id.user", - user: userId, - }, - password: password, - }, - }); - const json = await res.json(); - - return { - password, - accessToken: json.access_token, - userId: json.user_id, - deviceId: json.device_id, - homeServer: json.home_server, - }; - } - - public async setThreepid(userId: string, medium: string, address: string): Promise { - if (this.adminToken === undefined) { - const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true); - this.adminToken = result.accessToken; - } - - const url = `${this.config.baseUrl}/_synapse/admin/v2/users/${userId}`; - const res = await this.request.put(url, { - data: { - threepids: [ - { - medium, - address, - }, - ], - }, - headers: { - Authorization: `Bearer ${this.adminToken}`, - }, - }); - - if (!res.ok()) { - throw await res.json(); - } - } -} diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts new file mode 100644 index 00000000000..3561c1e6868 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -0,0 +1,48 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2023 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { Fixtures } from "@playwright/test"; +import { TestContainers } from "testcontainers"; + +import { Services } from "../../../services.ts"; +import { OAuthServer } from "../../oauth_server"; + +export const legacyOAuthHomeserver: Fixtures = { + _homeserver: async ({ _homeserver: container }, use) => { + const server = new OAuthServer(); + const port = server.start(); + + await TestContainers.exposeHostPorts(port); + container.withConfig({ + oidc_providers: [ + { + idp_id: "test", + idp_name: "OAuth test", + issuer: `http://localhost:${port}/oauth`, + authorization_endpoint: `http://localhost:${port}/oauth/auth.html`, + // the token endpoint receives requests from synapse, + // rather than the webapp, so needs to escape the docker container. + token_endpoint: `http://host.testcontainers.internal:${port}/oauth/token`, + userinfo_endpoint: `http://host.testcontainers.internal:${port}/oauth/userinfo`, + client_id: "synapse", + discover: false, + scopes: ["profile"], + skip_verification: true, + client_auth_method: "none", + user_mapping_provider: { + config: { + display_name_template: "{{ user.name }}", + }, + }, + }, + ], + }); + await use(container); + server.stop(); + }, +}; diff --git a/playwright/plugins/homeserver/synapse/templates/consent/res/templates/privacy/en/1.0.html b/playwright/plugins/homeserver/synapse/res/templates/privacy/en/1.0.html similarity index 100% rename from playwright/plugins/homeserver/synapse/templates/consent/res/templates/privacy/en/1.0.html rename to playwright/plugins/homeserver/synapse/res/templates/privacy/en/1.0.html diff --git a/playwright/plugins/homeserver/synapse/templates/consent/res/templates/privacy/en/success.html b/playwright/plugins/homeserver/synapse/res/templates/privacy/en/success.html similarity index 100% rename from playwright/plugins/homeserver/synapse/templates/consent/res/templates/privacy/en/success.html rename to playwright/plugins/homeserver/synapse/res/templates/privacy/en/success.html diff --git a/playwright/plugins/homeserver/synapse/templates/COPYME/README.md b/playwright/plugins/homeserver/synapse/templates/COPYME/README.md deleted file mode 100644 index df1ed89e6e4..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/COPYME/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Meta-template for synapse templates - -To make another template, you can copy this directory diff --git a/playwright/plugins/homeserver/synapse/templates/COPYME/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/COPYME/homeserver.yaml deleted file mode 100644 index cb58dc86615..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/COPYME/homeserver.yaml +++ /dev/null @@ -1,72 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -# XXX: This won't actually be right: it lets docker allocate an ephemeral port, -# so we have a chicken-and-egg problem -public_baseurl: http://localhost:8008/ -# Listener is always port 8008 (configured in the container) -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client, federation, consent] - compress: false - -# An sqlite in-memory database is fast & automatically wipes each time -database: - name: "sqlite3" - args: - database: ":memory:" - -# Needs to be configured to log to the console like a good docker process -log_config: "/data/log.config" - -rc_messages_per_second: 10000 -rc_message_burst_count: 10000 -rc_registration: - per_second: 10000 - burst_count: 10000 - -rc_login: - address: - per_second: 10000 - burst_count: 10000 - account: - per_second: 10000 - burst_count: 10000 - failed_attempts: - per_second: 10000 - burst_count: 10000 - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -enable_registration: true -enable_registration_without_verification: true -disable_msisdn_registration: false -# These placeholders will be be replaced with values generated at start -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -# Signing key must be here: it will be generated to this file -signing_key_path: "/data/localhost.signing.key" -email: - enable_notifs: false - smtp_host: "localhost" - smtp_port: 25 - smtp_user: "exampleusername" - smtp_pass: "examplepassword" - require_transport_security: False - notif_from: "Your Friendly %(app)s homeserver " - app_name: Matrix - notif_template_html: notif_mail.html - notif_template_text: notif_mail.txt - notif_for_new_users: True - client_base_url: "http://localhost/element" - -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true diff --git a/playwright/plugins/homeserver/synapse/templates/COPYME/log.config b/playwright/plugins/homeserver/synapse/templates/COPYME/log.config deleted file mode 100644 index ac232762da3..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/COPYME/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: INFO - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: INFO - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false diff --git a/playwright/plugins/homeserver/synapse/templates/consent/README.md b/playwright/plugins/homeserver/synapse/templates/consent/README.md deleted file mode 100644 index 713e55f9d51..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/consent/README.md +++ /dev/null @@ -1 +0,0 @@ -A synapse configured with user privacy consent enabled diff --git a/playwright/plugins/homeserver/synapse/templates/consent/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/consent/homeserver.yaml deleted file mode 100644 index d3a4fa520ca..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/consent/homeserver.yaml +++ /dev/null @@ -1,84 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -public_baseurl: "{{PUBLIC_BASEURL}}" -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client, federation, consent] - compress: false - -database: - name: "sqlite3" - args: - database: ":memory:" - -log_config: "/data/log.config" - -rc_messages_per_second: 10000 -rc_message_burst_count: 10000 -rc_registration: - per_second: 10000 - burst_count: 10000 - -rc_login: - address: - per_second: 10000 - burst_count: 10000 - account: - per_second: 10000 - burst_count: 10000 - failed_attempts: - per_second: 10000 - burst_count: 10000 - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -enable_registration: true -enable_registration_without_verification: true -disable_msisdn_registration: false -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -signing_key_path: "/data/localhost.signing.key" -email: - enable_notifs: false - smtp_host: "localhost" - smtp_port: 25 - smtp_user: "exampleusername" - smtp_pass: "examplepassword" - require_transport_security: False - notif_from: "Your Friendly %(app)s homeserver " - app_name: Matrix - notif_template_html: notif_mail.html - notif_template_text: notif_mail.txt - notif_for_new_users: True - client_base_url: "http://localhost/element" - -user_consent: - template_dir: /data/res/templates/privacy - version: 1.0 - server_notice_content: - msgtype: m.text - body: >- - To continue using this homeserver you must review and agree to the - terms and conditions at %(consent_uri)s - send_server_notice_to_guests: True - block_events_error: >- - To continue using this homeserver you must review and agree to the - terms and conditions at %(consent_uri)s - require_at_registration: true - -server_notices: - system_mxid_localpart: notices - system_mxid_display_name: "Server Notices" - system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ" - room_name: "Server Notices" -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true diff --git a/playwright/plugins/homeserver/synapse/templates/consent/log.config b/playwright/plugins/homeserver/synapse/templates/consent/log.config deleted file mode 100644 index b9123d0f5b9..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/consent/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: DEBUG - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: DEBUG - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false diff --git a/playwright/plugins/homeserver/synapse/templates/default/README.md b/playwright/plugins/homeserver/synapse/templates/default/README.md deleted file mode 100644 index 8f6b11f999b..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/default/README.md +++ /dev/null @@ -1 +0,0 @@ -A synapse configured with user privacy consent disabled diff --git a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml deleted file mode 100644 index 539a917b200..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml +++ /dev/null @@ -1,106 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -public_baseurl: "{{PUBLIC_BASEURL}}" -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client] - compress: false - -database: - name: "sqlite3" - args: - database: ":memory:" - -log_config: "/data/log.config" - -rc_messages_per_second: 10000 -rc_message_burst_count: 10000 -rc_registration: - per_second: 10000 - burst_count: 10000 -rc_joins: - local: - per_second: 9999 - burst_count: 9999 - remote: - per_second: 9999 - burst_count: 9999 -rc_joins_per_room: - per_second: 9999 - burst_count: 9999 -rc_3pid_validation: - per_second: 1000 - burst_count: 1000 - -rc_invites: - per_room: - per_second: 1000 - burst_count: 1000 - per_user: - per_second: 1000 - burst_count: 1000 - -rc_login: - address: - per_second: 10000 - burst_count: 10000 - account: - per_second: 10000 - burst_count: 10000 - failed_attempts: - per_second: 10000 - burst_count: 10000 - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -enable_registration: true -enable_registration_without_verification: true -disable_msisdn_registration: false -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -signing_key_path: "/data/localhost.signing.key" - -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true - -ui_auth: - session_timeout: "300s" - -oidc_providers: - - idp_id: test - idp_name: "OAuth test" - issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" - authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" - # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. - token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" - userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" - client_id: "synapse" - discover: false - scopes: ["profile"] - skip_verification: true - client_auth_method: none - user_mapping_provider: - config: - display_name_template: "{{ user.name }}" - -# Inhibit background updates as this Synapse isn't long-lived -background_updates: - min_batch_size: 100000 - sleep_duration_ms: 100000 - -experimental_features: - # Needed for e2e/crypto/crypto.spec.ts > Cryptography > decryption failure - # messages > non-joined historical messages. - # Can be removed after Synapse enables it by default - msc4115_membership_on_events: true - -enable_authenticated_media: true diff --git a/playwright/plugins/homeserver/synapse/templates/default/log.config b/playwright/plugins/homeserver/synapse/templates/default/log.config deleted file mode 100644 index b9123d0f5b9..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/default/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: DEBUG - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: DEBUG - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false diff --git a/playwright/plugins/homeserver/synapse/templates/dehydration/README.md b/playwright/plugins/homeserver/synapse/templates/dehydration/README.md deleted file mode 100644 index 18f7923e6d2..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/dehydration/README.md +++ /dev/null @@ -1 +0,0 @@ -A synapse configured with device dehydration v2 enabled diff --git a/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml deleted file mode 100644 index c3ac5d6536c..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/dehydration/homeserver.yaml +++ /dev/null @@ -1,102 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -public_baseurl: "{{PUBLIC_BASEURL}}" -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client] - compress: false - -database: - name: "sqlite3" - args: - database: ":memory:" - -log_config: "/data/log.config" - -rc_messages_per_second: 10000 -rc_message_burst_count: 10000 -rc_registration: - per_second: 10000 - burst_count: 10000 -rc_joins: - local: - per_second: 9999 - burst_count: 9999 - remote: - per_second: 9999 - burst_count: 9999 -rc_joins_per_room: - per_second: 9999 - burst_count: 9999 -rc_3pid_validation: - per_second: 1000 - burst_count: 1000 - -rc_invites: - per_room: - per_second: 1000 - burst_count: 1000 - per_user: - per_second: 1000 - burst_count: 1000 - -rc_login: - address: - per_second: 10000 - burst_count: 10000 - account: - per_second: 10000 - burst_count: 10000 - failed_attempts: - per_second: 10000 - burst_count: 10000 - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -enable_registration: true -enable_registration_without_verification: true -disable_msisdn_registration: false -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -signing_key_path: "/data/localhost.signing.key" - -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true - -ui_auth: - session_timeout: "300s" - -oidc_providers: - - idp_id: test - idp_name: "OAuth test" - issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" - authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" - # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. - token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" - userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" - client_id: "synapse" - discover: false - scopes: ["profile"] - skip_verification: true - client_auth_method: none - user_mapping_provider: - config: - display_name_template: "{{ user.name }}" - -# Inhibit background updates as this Synapse isn't long-lived -background_updates: - min_batch_size: 100000 - sleep_duration_ms: 100000 - -experimental_features: - msc2697_enabled: false - msc3814_enabled: true diff --git a/playwright/plugins/homeserver/synapse/templates/dehydration/log.config b/playwright/plugins/homeserver/synapse/templates/dehydration/log.config deleted file mode 100644 index b9123d0f5b9..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/dehydration/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: DEBUG - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: DEBUG - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false diff --git a/playwright/plugins/homeserver/synapse/templates/email/README.md b/playwright/plugins/homeserver/synapse/templates/email/README.md deleted file mode 100644 index 40c23ba0be4..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/email/README.md +++ /dev/null @@ -1 +0,0 @@ -A synapse configured to require an email for registration diff --git a/playwright/plugins/homeserver/synapse/templates/email/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/email/homeserver.yaml deleted file mode 100644 index fc20641ab40..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/email/homeserver.yaml +++ /dev/null @@ -1,44 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -public_baseurl: "{{PUBLIC_BASEURL}}" -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client] - compress: false - -database: - name: "sqlite3" - args: - database: ":memory:" - -log_config: "/data/log.config" - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -enable_registration: true -registrations_require_3pid: - - email -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -signing_key_path: "/data/localhost.signing.key" - -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true - -ui_auth: - session_timeout: "300s" - -email: - smtp_host: "%SMTP_HOST%" - smtp_port: %SMTP_PORT% - notif_from: "Your Friendly %(app)s homeserver " - app_name: my_branded_matrix_server diff --git a/playwright/plugins/homeserver/synapse/templates/email/log.config b/playwright/plugins/homeserver/synapse/templates/email/log.config deleted file mode 100644 index ac232762da3..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/email/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: INFO - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: INFO - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false diff --git a/playwright/plugins/homeserver/synapse/templates/guest-enabled/README.md b/playwright/plugins/homeserver/synapse/templates/guest-enabled/README.md deleted file mode 100644 index e1fef0b9d41..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/guest-enabled/README.md +++ /dev/null @@ -1 +0,0 @@ -A synapse configured with guest registration enabled. diff --git a/playwright/plugins/homeserver/synapse/templates/guest-enabled/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/guest-enabled/homeserver.yaml deleted file mode 100644 index 1faa39c3a74..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/guest-enabled/homeserver.yaml +++ /dev/null @@ -1,105 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -public_baseurl: "{{PUBLIC_BASEURL}}" -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client] - compress: false - -database: - name: "sqlite3" - args: - database: ":memory:" - -log_config: "/data/log.config" - -rc_messages_per_second: 10000 -rc_message_burst_count: 10000 -rc_registration: - per_second: 10000 - burst_count: 10000 -rc_joins: - local: - per_second: 9999 - burst_count: 9999 - remote: - per_second: 9999 - burst_count: 9999 -rc_joins_per_room: - per_second: 9999 - burst_count: 9999 -rc_3pid_validation: - per_second: 1000 - burst_count: 1000 - -rc_invites: - per_room: - per_second: 1000 - burst_count: 1000 - per_user: - per_second: 1000 - burst_count: 1000 - -rc_login: - address: - per_second: 10000 - burst_count: 10000 - account: - per_second: 10000 - burst_count: 10000 - failed_attempts: - per_second: 10000 - burst_count: 10000 - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -allow_guest_access: true -enable_registration: true -enable_registration_without_verification: true -disable_msisdn_registration: false -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -signing_key_path: "/data/localhost.signing.key" - -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true - -ui_auth: - session_timeout: "300s" - -oidc_providers: - - idp_id: test - idp_name: "OAuth test" - issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" - authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" - # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. - token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" - userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" - client_id: "synapse" - discover: false - scopes: ["profile"] - skip_verification: true - client_auth_method: none - user_mapping_provider: - config: - display_name_template: "{{ user.name }}" - -# Inhibit background updates as this Synapse isn't long-lived -background_updates: - min_batch_size: 100000 - sleep_duration_ms: 100000 - -experimental_features: - # Needed for e2e/crypto/crypto.spec.ts > Cryptography > decryption failure - # messages > non-joined historical messages. - # Can be removed after Synapse enables it by default - msc4115_membership_on_events: true diff --git a/playwright/plugins/homeserver/synapse/templates/guest-enabled/log.config b/playwright/plugins/homeserver/synapse/templates/guest-enabled/log.config deleted file mode 100644 index ac232762da3..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/guest-enabled/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: INFO - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: INFO - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/README.md b/playwright/plugins/homeserver/synapse/templates/mas-oidc/README.md deleted file mode 100644 index 223ff436a8d..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/mas-oidc/README.md +++ /dev/null @@ -1 +0,0 @@ -A synapse configured with auth delegated to via matrix authentication service diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml deleted file mode 100644 index 802d97acade..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml +++ /dev/null @@ -1,194 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -public_baseurl: "{{PUBLIC_BASEURL}}" -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client] - compress: false - -database: - name: "sqlite3" - args: - database: ":memory:" - -log_config: "/data/log.config" - -rc_messages_per_second: 10000 -rc_message_burst_count: 10000 -rc_registration: - per_second: 10000 - burst_count: 10000 -rc_joins: - local: - per_second: 9999 - burst_count: 9999 - remote: - per_second: 9999 - burst_count: 9999 -rc_joins_per_room: - per_second: 9999 - burst_count: 9999 -rc_3pid_validation: - per_second: 1000 - burst_count: 1000 - -rc_invites: - per_room: - per_second: 1000 - burst_count: 1000 - per_user: - per_second: 1000 - burst_count: 1000 - -rc_login: - address: - per_second: 10000 - burst_count: 10000 - account: - per_second: 10000 - burst_count: 10000 - failed_attempts: - per_second: 10000 - burst_count: 10000 - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -signing_key_path: "/data/localhost.signing.key" - -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true - -ui_auth: - session_timeout: "300s" - -# Inhibit background updates as this Synapse isn't long-lived -background_updates: - min_batch_size: 100000 - sleep_duration_ms: 100000 - -serve_server_wellknown: true -experimental_features: - msc3861: - enabled: true - - issuer: http://localhost:%MAS_PORT%/ - # We have to bake in the metadata here as we need to override `introspection_endpoint` - issuer_metadata: { - "issuer": "http://localhost:%MAS_PORT%/", - "authorization_endpoint": "http://localhost:%MAS_PORT%/authorize", - "token_endpoint": "http://localhost:%MAS_PORT%/oauth2/token", - "jwks_uri": "http://localhost:%MAS_PORT%/oauth2/keys.json", - "registration_endpoint": "http://localhost:%MAS_PORT%/oauth2/registration", - "scopes_supported": ["openid", "email"], - "response_types_supported": ["code", "id_token", "code id_token"], - "response_modes_supported": ["form_post", "query", "fragment"], - "grant_types_supported": - [ - "authorization_code", - "refresh_token", - "client_credentials", - "urn:ietf:params:oauth:grant-type:device_code", - ], - "token_endpoint_auth_methods_supported": - ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], - "token_endpoint_auth_signing_alg_values_supported": - [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "revocation_endpoint": "http://localhost:%MAS_PORT%/oauth2/revoke", - "revocation_endpoint_auth_methods_supported": - ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], - "revocation_endpoint_auth_signing_alg_values_supported": - [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - # This is the only changed value - "introspection_endpoint": "http://host.containers.internal:%MAS_PORT%/oauth2/introspect", - "introspection_endpoint_auth_methods_supported": - ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], - "introspection_endpoint_auth_signing_alg_values_supported": - [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "code_challenge_methods_supported": ["plain", "S256"], - "userinfo_endpoint": "http://localhost:%MAS_PORT%/oauth2/userinfo", - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": - ["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"], - "userinfo_signing_alg_values_supported": - ["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"], - "display_values_supported": ["page"], - "claim_types_supported": ["normal"], - "claims_supported": ["iss", "sub", "aud", "iat", "exp", "nonce", "auth_time", "at_hash", "c_hash"], - "claims_parameter_supported": false, - "request_parameter_supported": false, - "request_uri_parameter_supported": false, - "prompt_values_supported": ["none", "login", "create"], - "device_authorization_endpoint": "http://localhost:%MAS_PORT%/oauth2/device", - "org.matrix.matrix-authentication-service.graphql_endpoint": "http://localhost:%MAS_PORT%/graphql", - "account_management_uri": "http://localhost:%MAS_PORT%/account/", - "account_management_actions_supported": - [ - "org.matrix.profile", - "org.matrix.sessions_list", - "org.matrix.session_view", - "org.matrix.session_end", - ], - } - - # Matches the `client_id` in the auth service config - client_id: 0000000000000000000SYNAPSE - # Matches the `client_auth_method` in the auth service config - client_auth_method: client_secret_basic - # Matches the `client_secret` in the auth service config - client_secret: "SomeRandomSecret" - - # Matches the `matrix.secret` in the auth service config - admin_token: "AnotherRandomSecret" - - # URL to advertise to clients where users can self-manage their account - account_management_url: "http://localhost:%MAS_PORT%/account" diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/log.config b/playwright/plugins/homeserver/synapse/templates/mas-oidc/log.config deleted file mode 100644 index b9123d0f5b9..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/mas-oidc/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: DEBUG - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: DEBUG - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false diff --git a/playwright/plugins/mailhog/index.ts b/playwright/plugins/mailhog/index.ts deleted file mode 100644 index e9e5f08b717..00000000000 --- a/playwright/plugins/mailhog/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import mailhog from "mailhog"; - -import { getFreePort } from "../utils/port"; -import { Docker } from "../docker"; - -export interface Instance { - host: string; - smtpPort: number; - httpPort: number; - containerId: string; -} - -export class MailHogServer { - private readonly docker: Docker = new Docker(); - private instance?: Instance; - - async start(): Promise<{ api: mailhog.API; instance: Instance }> { - if (this.instance) throw new Error("Mailhog server is already running!"); - const smtpPort = await getFreePort(); - const httpPort = await getFreePort(); - console.log(`Starting mailhog...`); - const containerId = await this.docker.run({ - image: "mailhog/mailhog:latest", - containerName: `react-sdk-playwright-mailhog`, - params: ["-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`], - }); - console.log(`Started mailhog on ports smtp=${smtpPort} http=${httpPort}.`); - const host = await this.docker.getContainerIp(); - this.instance = { smtpPort, httpPort, containerId, host }; - return { api: mailhog({ host: "localhost", port: httpPort }), instance: this.instance }; - } - - async stop(): Promise { - if (!this.instance) throw new Error("Missing existing mailhog instance, did you call stop() before start()?"); - await this.docker.stop(); - console.log(`Stopped mailhog id ${this.instance.containerId}.`); - this.instance = undefined; - } -} diff --git a/playwright/plugins/matrix-authentication-service/config.yaml b/playwright/plugins/matrix-authentication-service/config.yaml deleted file mode 100644 index e7ab83e736e..00000000000 --- a/playwright/plugins/matrix-authentication-service/config.yaml +++ /dev/null @@ -1,153 +0,0 @@ -clients: - - client_id: 0000000000000000000SYNAPSE - client_auth_method: client_secret_basic - client_secret: "SomeRandomSecret" -http: - listeners: - - name: web - resources: - - name: discovery - - name: human - - name: oauth - - name: compat - - name: graphql - playground: true - - name: assets - path: /usr/local/share/mas-cli/assets/ - binds: - - address: "[::]:8080" - proxy_protocol: false - - name: internal - resources: - - name: health - binds: - - host: localhost - port: 8081 - proxy_protocol: false - trusted_proxies: - - 192.128.0.0/16 - - 172.16.0.0/12 - - 10.0.0.0/10 - - 127.0.0.1/8 - - fd00::/8 - - ::1/128 - public_base: "http://localhost:{{MAS_PORT}}/" - issuer: http://localhost:{{MAS_PORT}}/ -database: - host: "{{POSTGRES_HOST}}" - port: 5432 - database: postgres - username: postgres - password: "{{POSTGRES_PASSWORD}}" - max_connections: 10 - min_connections: 0 - connect_timeout: 30 - idle_timeout: 600 - max_lifetime: 1800 -telemetry: - tracing: - exporter: none - propagators: [] - metrics: - exporter: none - sentry: - dsn: null -templates: - path: /usr/local/share/mas-cli/templates/ - assets_manifest: /usr/local/share/mas-cli/manifest.json - translations_path: /usr/local/share/mas-cli/translations/ -email: - from: '"Authentication Service" ' - reply_to: '"Authentication Service" ' - transport: smtp - mode: plain - hostname: "host.containers.internal" - port: %{{SMTP_PORT}} - username: username - password: password - -secrets: - encryption: 984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5 - keys: - - kid: YEAhzrKipJ - key: | - -----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B - S79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/ - +/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki - OXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW - R+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA - uiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83 - CdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8 - z8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv - x2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w - VkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK - UdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F - vYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7 - XnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4 - cgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V - 4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT - hr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V - 5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN - yO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ - NghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw - b4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/ - /fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH - fjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt - +57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ - 1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m - MC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq - -----END RSA PRIVATE KEY----- - - kid: 8J1AxrlNZT - key: | - -----BEGIN EC PRIVATE KEY----- - MHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49 - AwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW - dE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw== - -----END EC PRIVATE KEY----- - - kid: 3BW6un1EBi - key: | - -----BEGIN EC PRIVATE KEY----- - MIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2 - q3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK - mZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P - 9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs= - -----END EC PRIVATE KEY----- - - kid: pkZ0pTKK0X - key: | - -----BEGIN EC PRIVATE KEY----- - MHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK - oUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl - Aer+6PMZpPc8ycyeH9N+U9NAyliBhQ== - -----END EC PRIVATE KEY----- -passwords: - enabled: true - schemes: - - version: 1 - algorithm: argon2id -matrix: - homeserver: localhost - secret: AnotherRandomSecret - endpoint: "{{SYNAPSE_URL}}" -policy: - wasm_module: /usr/local/share/mas-cli/policy.wasm - client_registration_entrypoint: client_registration/violation - register_entrypoint: register/violation - authorization_grant_entrypoint: authorization_grant/violation - password_entrypoint: password/violation - email_entrypoint: email/violation - data: - client_registration: - allow_insecure_uris: true # allow non-SSL and localhost URIs - allow_missing_contacts: true # EW doesn't have contacts at this time -upstream_oauth2: - providers: [] -branding: - service_name: null - policy_uri: null - tos_uri: null - imprint: null - logo_uri: null -experimental: - access_token_ttl: 300 - compat_token_ttl: 300 diff --git a/playwright/plugins/matrix-authentication-service/index.ts b/playwright/plugins/matrix-authentication-service/index.ts deleted file mode 100644 index eaad350b829..00000000000 --- a/playwright/plugins/matrix-authentication-service/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import path, { basename } from "node:path"; -import os from "node:os"; -import * as fse from "fs-extra"; -import { BrowserContext, TestInfo } from "@playwright/test"; - -import { getFreePort } from "../utils/port"; -import { Docker } from "../docker"; -import { PG_PASSWORD, PostgresDocker } from "../postgres"; -import { HomeserverInstance } from "../homeserver"; -import { Instance as MailhogInstance } from "../mailhog"; - -// Docker tag to use for `ghcr.io/matrix-org/matrix-authentication-service` image. -// We use a debug tag so that we have a shell and can run all 3 necessary commands in one run. -const TAG = "0.8.0-debug"; - -export interface ProxyInstance { - containerId: string; - postgresId: string; - configDir: string; - port: number; -} - -async function cfgDirFromTemplate(opts: { - postgresHost: string; - synapseUrl: string; - masPort: string; - smtpPort: string; -}): Promise<{ - configDir: string; -}> { - const configPath = path.join(__dirname, "config.yaml"); - const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-mas-")); - - const outputHomeserver = path.join(tempDir, "config.yaml"); - console.log(`Gen ${configPath} -> ${outputHomeserver}`); - let config = await fse.readFile(configPath, "utf8"); - config = config.replace(/{{MAS_PORT}}/g, opts.masPort); - config = config.replace(/{{POSTGRES_HOST}}/g, opts.postgresHost); - config = config.replace(/{{POSTGRES_PASSWORD}}/g, PG_PASSWORD); - config = config.replace(/%{{SMTP_PORT}}/g, opts.smtpPort); - config = config.replace(/{{SYNAPSE_URL}}/g, opts.synapseUrl); - - await fse.writeFile(outputHomeserver, config); - - // Allow anyone to read, write and execute in the temp directory - // so that the DIND setup that we use to update the playwright screenshots work without any issues. - await fse.chmod(tempDir, 0o757); - - return { - configDir: tempDir, - }; -} - -export class MatrixAuthenticationService { - private readonly masDocker = new Docker(); - private readonly postgresDocker = new PostgresDocker("mas"); - private instance: ProxyInstance; - public port: number; - - constructor(private context: BrowserContext) {} - - async prepare(): Promise<{ port: number }> { - this.port = await getFreePort(); - return { port: this.port }; - } - - async start(homeserver: HomeserverInstance, mailhog: MailhogInstance): Promise { - console.log(new Date(), "Starting mas..."); - - if (!this.port) await this.prepare(); - const port = this.port; - const { containerId: postgresId, ipAddress: postgresIp } = await this.postgresDocker.start(); - const { configDir } = await cfgDirFromTemplate({ - masPort: port.toString(), - postgresHost: postgresIp, - synapseUrl: homeserver.config.dockerUrl, - smtpPort: mailhog.smtpPort.toString(), - }); - - console.log(new Date(), "starting mas container...", TAG); - const containerId = await this.masDocker.run({ - image: "ghcr.io/matrix-org/matrix-authentication-service:" + TAG, - containerName: "react-sdk-playwright-mas", - params: ["-p", `${port}:8080/tcp`, "-v", `${configDir}:/config`, "--entrypoint", "sh"], - cmd: [ - "-c", - "mas-cli database migrate --config /config/config.yaml && " + - "mas-cli config sync --config /config/config.yaml && " + - "mas-cli server --config /config/config.yaml", - ], - }); - console.log(new Date(), "started!"); - - // Set up redirects - const baseUrl = `http://localhost:${port}`; - for (const path of [ - "**/_matrix/client/*/login", - "**/_matrix/client/*/login/**", - "**/_matrix/client/*/logout", - "**/_matrix/client/*/refresh", - ]) { - await this.context.route(path, async (route) => { - await route.continue({ - url: new URL(route.request().url().split("/").slice(3).join("/"), baseUrl).href, - }); - }); - } - - this.instance = { containerId, postgresId, port, configDir }; - return this.instance; - } - - async stop(testInfo: TestInfo): Promise { - if (!this.instance) return; // nothing to stop - const id = this.instance.containerId; - const logPath = path.join("playwright", "logs", "matrix-authentication-service", id); - await fse.ensureDir(logPath); - await this.masDocker.persistLogsToFile({ - stdoutFile: path.join(logPath, "stdout.log"), - stderrFile: path.join(logPath, "stderr.log"), - }); - - await this.masDocker.stop(); - await this.postgresDocker.stop(); - - if (testInfo.status !== "passed") { - const logs = [path.join(logPath, "stdout.log"), path.join(logPath, "stderr.log")]; - for (const path of logs) { - await testInfo.attach(`mas-${basename(path)}`, { - path, - contentType: "text/plain", - }); - } - await testInfo.attach("mas-config.yaml", { - path: path.join(this.instance.configDir, "config.yaml"), - contentType: "text/plain", - }); - } - - await fse.remove(this.instance.configDir); - console.log(new Date(), "Stopped mas."); - } -} diff --git a/playwright/plugins/postgres/index.ts b/playwright/plugins/postgres/index.ts deleted file mode 100644 index 23d37ffef67..00000000000 --- a/playwright/plugins/postgres/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Docker } from "../docker"; - -export const PG_PASSWORD = "p4S5w0rD"; - -/** - * Class to manage a postgres database in docker - */ -export class PostgresDocker extends Docker { - /** - * @param key an opaque string to use when naming the docker containers instantiated by this class - */ - public constructor(private key: string) { - super(); - } - - private async waitForPostgresReady(ipAddress: string): Promise { - const waitTimeMillis = 30000; - const startTime = new Date().getTime(); - let lastErr: Error | null = null; - while (new Date().getTime() - startTime < waitTimeMillis) { - try { - // Note that we specify the IP address rather than letting it connect to the local - // socket: that's the listener we care about and empirically it matters. - await this.exec(["pg_isready", "-h", ipAddress, "-U", "postgres"], true); - lastErr = null; - break; - } catch (err) { - console.log("pg_isready: failed"); - lastErr = err; - } - } - if (lastErr) { - console.log("rethrowing"); - throw lastErr; - } - } - - public async start(): Promise<{ - ipAddress: string; - containerId: string; - }> { - console.log(new Date(), "starting postgres container"); - const containerId = await this.run({ - image: "postgres", - containerName: `react-sdk-playwright-postgres-${this.key}`, - params: ["--tmpfs=/pgtmpfs", "-e", "PGDATA=/pgtmpfs", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`], - // Optimise for testing - https://www.postgresql.org/docs/current/non-durability.html - cmd: ["-c", `fsync=off`, "-c", `synchronous_commit=off`, "-c", `full_page_writes=off`], - }); - - const ipAddress = await this.getContainerIp(); - console.log(new Date(), "postgres container up"); - - await this.waitForPostgresReady(ipAddress); - console.log(new Date(), "postgres container ready"); - return { ipAddress, containerId }; - } -} diff --git a/playwright/plugins/sliding-sync-proxy/index.ts b/playwright/plugins/sliding-sync-proxy/index.ts deleted file mode 100644 index 81bfe79fc10..00000000000 --- a/playwright/plugins/sliding-sync-proxy/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import type { BrowserContext, Route } from "@playwright/test"; -import { getFreePort } from "../utils/port"; -import { Docker } from "../docker"; -import { PG_PASSWORD, PostgresDocker } from "../postgres"; - -// Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image. -const SLIDING_SYNC_PROXY_TAG = "v0.99.3"; - -export interface ProxyInstance { - containerId: string; - postgresId: string; - port: number; -} - -export class SlidingSyncProxy { - private readonly proxyDocker = new Docker(); - private readonly postgresDocker = new PostgresDocker("sliding-sync"); - private instance: ProxyInstance; - - constructor( - private synapseIp: string, - private context: BrowserContext, - ) {} - - private syncHandler = async (route: Route) => { - if (!this.instance) return route.abort("blockedbyclient"); - - const baseUrl = `http://localhost:${this.instance.port}`; - await route.continue({ - url: new URL(route.request().url().split("/").slice(3).join("/"), baseUrl).href, - }); - }; - - async start(): Promise { - console.log(new Date(), "Starting sliding sync proxy..."); - - const { ipAddress: postgresIp, containerId: postgresId } = await this.postgresDocker.start(); - - const port = await getFreePort(); - console.log(new Date(), "starting proxy container...", SLIDING_SYNC_PROXY_TAG); - const containerId = await this.proxyDocker.run({ - image: "ghcr.io/matrix-org/sliding-sync:" + SLIDING_SYNC_PROXY_TAG, - containerName: "react-sdk-playwright-sliding-sync-proxy", - params: [ - "-p", - `${port}:8008/tcp`, - "-e", - "SYNCV3_SECRET=bwahahaha", - "-e", - `SYNCV3_SERVER=${this.synapseIp}`, - "-e", - `SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`, - ], - }); - console.log(new Date(), "started!"); - - this.instance = { containerId, postgresId, port }; - await this.context.route("**/_matrix/client/unstable/org.matrix.msc3575/sync*", this.syncHandler); - return this.instance; - } - - async stop(): Promise { - await this.context.unroute("**/_matrix/client/unstable/org.matrix.msc3575/sync*", this.syncHandler); - - await this.postgresDocker.stop(); - await this.proxyDocker.stop(); - console.log(new Date(), "Stopped sliding sync proxy."); - } -} diff --git a/playwright/services.ts b/playwright/services.ts new file mode 100644 index 00000000000..bf4bad18fcc --- /dev/null +++ b/playwright/services.ts @@ -0,0 +1,113 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { test as base } from "@playwright/test"; +import mailhog from "mailhog"; +import { GenericContainer, Network, StartedNetwork, StartedTestContainer, Wait } from "testcontainers"; +import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql"; + +import { StartedSynapseContainer, SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts"; +import { MatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; + +export interface Services { + network: StartedNetwork; + postgres: StartedPostgreSqlContainer; + + mailhog: StartedTestContainer; + mailhogClient: mailhog.API; + + synapseConfigOptions: SynapseConfigOptions; + _homeserver: SynapseContainer; + homeserver: StartedSynapseContainer; + mas: StartedTestContainer; +} + +// TODO logs +export const test = base.extend({ + // eslint-disable-next-line no-empty-pattern + network: async ({}, use) => { + const network = await new Network().start(); + await use(network); + await network.stop(); + }, + postgres: async ({ network }, use) => { + const container = await new PostgreSqlContainer() + .withNetwork(network) + .withNetworkAliases("postgres") + .withTmpFs({ + "/dev/shm/pgdata/data": "", + }) + .withEnvironment({ + PG_DATA: "/dev/shm/pgdata/data", + }) + .withCommand([ + "-c", + "shared_buffers=128MB", + "-c", + `fsync=off`, + "-c", + `synchronous_commit=off`, + "-c", + "full_page_writes=off", + ]) + .start(); + await use(container); + await container.stop(); + }, + + mailhog: async ({ network }, use) => { + const container = await new GenericContainer("mailhog/mailhog:latest") + .withNetwork(network) + .withNetworkAliases("mailhog") + .withExposedPorts(8025) + .withWaitStrategy(Wait.forListeningPorts()) + .start(); + await use(container); + await container.stop(); + }, + mailhogClient: async ({ mailhog: container }, use) => { + await use(mailhog({ host: container.getHost(), port: container.getMappedPort(8025) })); + }, + + synapseConfigOptions: [{}, { option: true }], + _homeserver: async ({ request }, use) => { + const container = new SynapseContainer(request); + await use(container); + }, + homeserver: async ({ network, _homeserver: homeserver, synapseConfigOptions }, use) => { + const container = await homeserver + .withNetwork(network) + .withNetworkAliases("homeserver") + .withConfig(synapseConfigOptions) + .start(); + + await use(container); + await container.stop(); + }, + mas: async ({ network, homeserver }, use) => { + const container = await new MatrixAuthenticationServiceContainer() + .withNetwork(network) + .withNetworkAliases("mas") + .withConfig({ + clients: [ + { + client_id: "0000000000000000000SYNAPSE", + client_auth_method: "client_secret_basic", + client_secret: "SomeRandomSecret", + }, + ], + matrix: { + homeserver: "localhost", + secret: "AnotherRandomSecret", + endpoint: "http://synapse:8008", + }, + }) + .start(); + await use(container); + await container.stop(); + }, +}); diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts new file mode 100644 index 00000000000..e073fbcada9 --- /dev/null +++ b/playwright/testcontainers/dendrite.ts @@ -0,0 +1,280 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { GenericContainer, Wait } from "testcontainers"; +import { APIRequestContext } from "@playwright/test"; +import * as YAML from "yaml"; +import { deepCopy } from "matrix-js-sdk/src/utils"; +import { set } from "lodash"; + +import { getFreePort } from "../plugins/utils/port.ts"; +import { randB64Bytes } from "../plugins/utils/rand.ts"; +import { StartedSynapseContainer } from "./synapse.ts"; + +const DEFAULT_CONFIG = { + version: 2, + global: { + server_name: "localhost", + private_key: "matrix_key.pem", + old_private_keys: null, + key_validity_period: "168h0m0s", + cache: { + max_size_estimated: "1gb", + max_age: "1h", + }, + well_known_server_name: "", + well_known_client_name: "", + trusted_third_party_id_servers: ["matrix.org", "vector.im"], + disable_federation: false, + presence: { + enable_inbound: false, + enable_outbound: false, + }, + report_stats: { + enabled: false, + endpoint: "https://matrix.org/report-usage-stats/push", + }, + server_notices: { + enabled: false, + local_part: "_server", + display_name: "Server Alerts", + avatar_url: "", + room_name: "Server Alerts", + }, + jetstream: { + addresses: null, + disable_tls_validation: false, + storage_path: "./", + topic_prefix: "Dendrite", + }, + metrics: { + enabled: false, + basic_auth: { + username: "metrics", + password: "metrics", + }, + }, + dns_cache: { + enabled: false, + cache_size: 256, + cache_lifetime: "5m", + }, + }, + app_service_api: { + disable_tls_validation: false, + config_files: null, + }, + client_api: { + registration_disabled: false, + guests_disabled: true, + registration_shared_secret: "secret", + enable_registration_captcha: false, + recaptcha_public_key: "", + recaptcha_private_key: "", + recaptcha_bypass_secret: "", + turn: { + turn_user_lifetime: "5m", + turn_uris: null, + turn_shared_secret: "", + }, + rate_limiting: { + enabled: true, + threshold: 20, + cooloff_ms: 500, + exempt_user_ids: null, + }, + }, + federation_api: { + send_max_retries: 16, + disable_tls_validation: false, + disable_http_keepalives: false, + key_perspectives: [ + { + server_name: "matrix.org", + keys: [ + { + key_id: "ed25519:auto", + public_key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw", + }, + { + key_id: "ed25519:a_RXGa", + public_key: "l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ", + }, + ], + }, + ], + prefer_direct_fetch: false, + database: { + connection_string: "file:dendrite-federationapi.db", + }, + }, + media_api: { + base_path: "./media_store", + max_file_size_bytes: 10485760, + dynamic_thumbnails: false, + max_thumbnail_generators: 10, + thumbnail_sizes: [ + { + width: 32, + height: 32, + method: "crop", + }, + { + width: 96, + height: 96, + method: "crop", + }, + { + width: 640, + height: 480, + method: "scale", + }, + ], + database: { + connection_string: "file:dendrite-mediaapi.db", + }, + }, + mscs: { + mscs: null, + database: { + connection_string: "file:dendrite-msc.db", + }, + }, + sync_api: { + search: { + enabled: false, + index_path: "./searchindex", + language: "en", + }, + database: { + connection_string: "file:dendrite-syncapi.db", + }, + }, + user_api: { + bcrypt_cost: 10, + auto_join_rooms: null, + account_database: { + connection_string: "file:dendrite-userapi.db", + }, + }, + room_server: { + database: { + connection_string: "file:dendrite-roomserverapi.db", + }, + }, + key_server: { + database: { + connection_string: "file:dendrite-keyserverapi.db", + }, + }, + relay_api: { + database: { + connection_string: "file:dendrite-relayapi.db", + }, + }, + tracing: { + enabled: false, + jaeger: { + serviceName: "", + disabled: false, + rpc_metrics: false, + tags: [], + sampler: null, + reporter: null, + headers: null, + baggage_restrictions: null, + throttler: null, + }, + }, + logging: [ + { + type: "std", + level: "debug", + }, + { + type: "file", + level: "debug", + params: { + path: "./logs", + }, + }, + ], +}; + +export class DendriteContainer extends GenericContainer { + private config: typeof DEFAULT_CONFIG; + + constructor( + private request: APIRequestContext, + image = "matrixdotorg/dendrite-monolith:main", + entrypoint = "/usr/bin/dendrite", + ) { + super(image); + + this.config = deepCopy(DEFAULT_CONFIG); + this.config.client_api.registration_shared_secret = randB64Bytes(16); + + this.withEntrypoint([entrypoint]) + .withCommand([ + "--config", + "/etc/dendrite/dendrite.yaml", + "--really-enable-open-registration", + "true", + "run", + ]) + .withWaitStrategy(Wait.forHttp("/_matrix/client/versions", 8008)); + + // const docker = new Docker(); + // await docker.run({ + // image: dendriteImage, + // params: ["--entrypoint=", "-v", `${tempDir}:/mnt`], + // containerName: `react-sdk-playwright-dendrite-keygen`, + // cmd: ["/usr/bin/generate-keys", "-private-key", "/mnt/matrix_key.pem"], + // }); + } + + public withConfigField(key: string, value: any): this { + set(this.config, key, value); + return this; + } + + public withConfig(config: Partial): this { + this.config = { + ...this.config, + ...config, + }; + return this; + } + + public override async start(): Promise { + const port = await getFreePort(); + + this.withExposedPorts({ + container: 8008, + host: port, + }).withCopyContentToContainer([ + { + target: "/etc/dendrite/dendrite.yaml", + content: YAML.stringify(this.config), + }, + ]); + + // Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it + return new StartedSynapseContainer( + await super.start(), + `http://localhost:${port}`, + this.config.client_api.registration_shared_secret, + this.request, + ); + } +} + +export class PineconeContainer extends DendriteContainer { + constructor(request: APIRequestContext) { + super(request, "matrixdotorg/dendrite-demo-pinecone:main", "/usr/bin/dendrite-demo-pinecone"); + } +} diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts new file mode 100644 index 00000000000..b4e5bf75377 --- /dev/null +++ b/playwright/testcontainers/mas.ts @@ -0,0 +1,206 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import crypto from "node:crypto"; +import * as YAML from "yaml"; + +import { getFreePort } from "../plugins/utils/port.ts"; +import { Credentials } from "../plugins/homeserver"; +import { deepCopy } from "matrix-js-sdk/src/utils.ts"; + +const DEFAULT_CONFIG = { + http: { + listeners: [ + { + name: "web", + resources: [ + { + name: "discovery", + }, + { + name: "human", + }, + { + name: "oauth", + }, + { + name: "compat", + }, + { + name: "graphql", + playground: true, + }, + { + name: "assets", + path: "/usr/local/share/mas-cli/assets/", + }, + ], + binds: [ + { + address: "[::]:8080", + }, + ], + proxy_protocol: false, + }, + { + name: "internal", + resources: [ + { + name: "health", + }, + ], + binds: [ + { + host: "localhost", + port: 8081, + }, + ], + proxy_protocol: false, + }, + ], + trusted_proxies: ["192.128.0.0/16", "172.16.0.0/12", "10.0.0.0/10", "127.0.0.1/8", "fd00::/8", "::1/128"], + public_base: "", // Needs to be set + issuer: "", // Needs to be set + }, + database: { + host: "postgres", + port: 5432, + database: "postgres", + username: "postgres", + password: "p4S5w0rD", + max_connections: 10, + min_connections: 0, + connect_timeout: 30, + idle_timeout: 600, + max_lifetime: 1800, + }, + telemetry: { + tracing: { + exporter: "none", + propagators: [], + }, + metrics: { + exporter: "none", + }, + sentry: { + dsn: null, + }, + }, + templates: { + path: "/usr/local/share/mas-cli/templates/", + assets_manifest: "/usr/local/share/mas-cli/manifest.json", + translations_path: "/usr/local/share/mas-cli/translations/", + }, + email: { + from: '"Authentication Service" ', + reply_to: '"Authentication Service" ', + transport: "smtp", + mode: "plain", + hostname: "mailhog", + port: 1025, + username: "username", + password: "password", + }, + secrets: { + encryption: "984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5", + keys: [ + { + kid: "YEAhzrKipJ", + key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B\nS79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/\n+/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki\nOXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW\nR+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA\nuiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83\nCdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8\nz8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv\nx2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w\nVkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK\nUdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F\nvYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7\nXnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4\ncgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V\n4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT\nhr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V\n5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN\nyO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ\nNghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw\nb4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/\n/fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH\nfjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt\n+57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ\n1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m\nMC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq\n-----END RSA PRIVATE KEY-----\n", + }, + { + kid: "8J1AxrlNZT", + key: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49\nAwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW\ndE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw==\n-----END EC PRIVATE KEY-----\n", + }, + { + kid: "3BW6un1EBi", + key: "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2\nq3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK\nmZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P\n9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs=\n-----END EC PRIVATE KEY-----\n", + }, + { + kid: "pkZ0pTKK0X", + key: "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK\noUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl\nAer+6PMZpPc8ycyeH9N+U9NAyliBhQ==\n-----END EC PRIVATE KEY-----\n", + }, + ], + }, + passwords: { + enabled: true, + schemes: [ + { + version: 1, + algorithm: "argon2id", + }, + ], + }, + policy: { + wasm_module: "/usr/local/share/mas-cli/policy.wasm", + client_registration_entrypoint: "client_registration/violation", + register_entrypoint: "register/violation", + authorization_grant_entrypoint: "authorization_grant/violation", + password_entrypoint: "password/violation", + email_entrypoint: "email/violation", + data: { + client_registration: { + allow_insecure_uris: true, + allow_missing_contacts: true, + }, + }, + }, + upstream_oauth2: { + providers: [], + }, + branding: { + service_name: null, + policy_uri: null, + tos_uri: null, + imprint: null, + logo_uri: null, + }, + experimental: { + access_token_ttl: 300, + compat_token_ttl: 300, + }, +}; + +export class MatrixAuthenticationServiceContainer extends GenericContainer { + private config: typeof DEFAULT_CONFIG; + + constructor() { + super("ghcr.io/matrix-org/matrix-authentication-service:0.8.0"); + + this.config = deepCopy(DEFAULT_CONFIG); + + this.withWaitStrategy(Wait.forHttp("/health", 8081)).withCommand(["server", "--config", "/config/config.yaml"]); + } + + public withConfig(config: object): this { + this.config = { + ...this.config, + ...config, + }; + return this; + } + + public override async start(): Promise { + const port = await getFreePort(); + + this.config.http.public_base = `http://localhost:${port}/`; + this.config.http.issuer = `http://localhost:${port}/`; + + this.withExposedPorts({ + container: 8080, + host: port, + }).withCopyContentToContainer([ + { + target: "/config/config.yaml", + content: YAML.stringify(this.config), + }, + ]); + + return super.start(); + } +} diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts new file mode 100644 index 00000000000..c3cb0ab69fa --- /dev/null +++ b/playwright/testcontainers/synapse.ts @@ -0,0 +1,323 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { APIRequestContext } from "@playwright/test"; +import crypto from "node:crypto"; +import * as YAML from "yaml"; +import { deepCopy } from "matrix-js-sdk/src/utils"; +import { set } from "lodash"; + +import { getFreePort } from "../plugins/utils/port.ts"; +import { randB64Bytes } from "../plugins/utils/rand.ts"; +import { Credentials, HomeserverInstance } from "../plugins/homeserver"; + +const TAG = "develop@sha256:17cc0a301447430624afb860276e5c13270ddeb99a3f6d1c6d519a20b1a8f650"; + +const DEFAULT_CONFIG = { + server_name: "localhost", + public_baseurl: "", + pid_file: "/homeserver.pid", + web_client: false, + soft_file_limit: 0, + log_config: "/data/log.config", + listeners: [ + { + port: 8008, + tls: false, + bind_addresses: ["::"], + type: "http", + x_forwarded: true, + resources: [ + { + names: ["client"], + compress: false, + }, + ], + }, + ], + database: { + name: "sqlite3", + args: { + database: ":memory:", + }, + }, + rc_messages_per_second: 10000, + rc_message_burst_count: 10000, + rc_registration: { + per_second: 10000, + burst_count: 10000, + }, + rc_joins: { + local: { + per_second: 9999, + burst_count: 9999, + }, + remote: { + per_second: 9999, + burst_count: 9999, + }, + }, + rc_joins_per_room: { + per_second: 9999, + burst_count: 9999, + }, + rc_3pid_validation: { + per_second: 1000, + burst_count: 1000, + }, + rc_invites: { + per_room: { + per_second: 1000, + burst_count: 1000, + }, + per_user: { + per_second: 1000, + burst_count: 1000, + }, + }, + rc_login: { + address: { + per_second: 10000, + burst_count: 10000, + }, + account: { + per_second: 10000, + burst_count: 10000, + }, + failed_attempts: { + per_second: 10000, + burst_count: 10000, + }, + }, + media_store_path: "/media", + max_upload_size: "50M", + max_image_pixels: "32M", + dynamic_thumbnails: false, + enable_registration: true, + enable_registration_without_verification: true, + disable_msisdn_registration: false, + registrations_require_3pid: [], + registration_shared_secret: "secret", + enable_metrics: false, + report_stats: false, + macaroon_secret_key: "secret", + form_secret: "secret", + signing_key_path: "/data/localhost.signing.key", + trusted_key_servers: [], + password_config: { + enabled: true, + }, + ui_auth: { + session_timeout: "300s", + }, + background_updates: { + min_batch_size: 100000, + sleep_duration_ms: 100000, + }, + enable_authenticated_media: true, + email: undefined, + user_consent: undefined, + server_notices: undefined, + allow_guest_access: false, + experimental_features: {}, + oidc_providers: [], + serve_server_wellknown: true, +}; + +export type SynapseConfigOptions = Partial; + +export class SynapseContainer extends GenericContainer { + private config: typeof DEFAULT_CONFIG; + + constructor(private readonly request: APIRequestContext) { + super(`ghcr.io/element-hq/synapse:${TAG}`); + + this.config = deepCopy(DEFAULT_CONFIG); + this.config.registration_shared_secret = randB64Bytes(16); + this.config.macaroon_secret_key = randB64Bytes(16); + this.config.form_secret = randB64Bytes(16); + + const signingKey = randB64Bytes(32); + this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([ + { target: "/data/localhost.signing.key", content: `ed25519 x ${signingKey}` }, + { + target: "/data/log.config", + content: YAML.stringify({ + version: 1, + formatters: { + precise: { + format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s", + }, + }, + handlers: { + console: { + class: "logging.StreamHandler", + formatter: "precise", + }, + }, + loggers: { + "synapse.storage.SQL": { + level: "DEBUG", + }, + "twisted": { + handlers: ["console"], + propagate: false, + }, + }, + root: { + level: "DEBUG", + handlers: ["console"], + }, + disable_existing_loggers: false, + }), + }, + ]); + } + + public withConfigField(key: string, value: any): this { + set(this.config, key, value); + return this; + } + + public withConfig(config: Partial): this { + this.config = { + ...this.config, + ...config, + }; + return this; + } + + public override async start(): Promise { + const port = await getFreePort(); + + this.withExposedPorts({ + container: 8008, + host: port, + }) + .withConfig({ + public_baseurl: `http://localhost:${port}`, + }) + .withCopyContentToContainer([ + { + target: "/data/homeserver.yaml", + content: YAML.stringify(this.config), + }, + ]); + + return new StartedSynapseContainer( + await super.start(), + `http://localhost:${port}`, + this.config.registration_shared_secret, + this.request, + ); + } +} + +export class StartedSynapseContainer extends AbstractStartedContainer implements HomeserverInstance { + private adminToken?: string; + + constructor( + container: StartedTestContainer, + public readonly baseUrl: string, + private readonly registrationSharedSecret: string, + private readonly request: APIRequestContext, + ) { + super(container); + } + + private async registerUserInternal( + username: string, + password: string, + displayName?: string, + admin = false, + ): Promise { + const url = `${this.baseUrl}/_synapse/admin/v1/register`; + const { nonce } = await this.request.get(url).then((r) => r.json()); + const mac = crypto + .createHmac("sha1", this.registrationSharedSecret) + .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) + .digest("hex"); + const res = await this.request.post(url, { + data: { + nonce, + username, + password, + mac, + admin, + displayname: displayName, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + + const data = await res.json(); + return { + homeServer: data.home_server, + accessToken: data.access_token, + userId: data.user_id, + deviceId: data.device_id, + password, + displayName, + }; + } + + public registerUser(username: string, password: string, displayName?: string): Promise { + return this.registerUserInternal(username, password, displayName, false); + } + + public async loginUser(userId: string, password: string): Promise { + const url = `${this.baseUrl}/_matrix/client/v3/login`; + const res = await this.request.post(url, { + data: { + type: "m.login.password", + identifier: { + type: "m.id.user", + user: userId, + }, + password: password, + }, + }); + const json = await res.json(); + + return { + password, + accessToken: json.access_token, + userId: json.user_id, + deviceId: json.device_id, + homeServer: json.home_server, + }; + } + + public async setThreepid(userId: string, medium: string, address: string): Promise { + if (this.adminToken === undefined) { + const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true); + this.adminToken = result.accessToken; + } + + const url = `${this.baseUrl}/_synapse/admin/v2/users/${userId}`; + const res = await this.request.put(url, { + data: { + threepids: [ + { + medium, + address, + }, + ], + }, + headers: { + Authorization: `Bearer ${this.adminToken}`, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + } +} diff --git a/yarn.lock b/yarn.lock index e72b11a5ede..a68b8cf97ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1158,6 +1158,11 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1542,6 +1547,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + "@floating-ui/core@^1.6.0": version "1.6.8" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12" @@ -2076,6 +2086,11 @@ tslib "^2.6.2" webcrypto-core "^1.8.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@playwright/test@^1.40.1": version "1.49.1" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827" @@ -2657,6 +2672,13 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" +"@testcontainers/postgresql@^10.16.0": + version "10.16.0" + resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-10.16.0.tgz#0437a9b426d64ea958e745a0e2ae19462b786f81" + integrity sha512-zWFQI+3QxlEELRvVv27i6zlVEPNUz9zKaSh7iWmFlCdfhcyr78daS0FG8FIfdQ79VK7YXA4jv+dTYXa2SwXu/w== + dependencies: + testcontainers "^10.16.0" + "@testing-library/dom@^10.4.0": version "10.4.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" @@ -2824,6 +2846,23 @@ resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz#dcef10a69d357fe9d43ac4ff2eca6b85dbf466af" integrity sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg== +"@types/docker-modem@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/docker-modem/-/docker-modem-3.0.6.tgz#1f9262fcf85425b158ca725699a03eb23cddbf87" + integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== + dependencies: + "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.3.29": + version "3.3.33" + resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.33.tgz#67d9b4223caf41a0735695abe89c292e05d305c9" + integrity sha512-7av8lVOhkW7Xd11aZTSq5zhdpyNraldXwQR0pxUCiSNTvIzsP86KrFrmrZgxtrXD2Zrtzwt4H6OYLbATONWzWg== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" + "@types/ssh2" "*" + "@types/escape-html@^1.0.1": version "1.0.4" resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.4.tgz#dc7c166b76c7b03b27e32f80edf01d91eb5d9af2" @@ -3088,6 +3127,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^18.11.18": + version "18.19.69" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.69.tgz#748d301818ba4b238854c53d290257a70aae7d01" + integrity sha512-ECPdY1nlaiO/Y6GUnwgtAAhLNaQ53AyIVz+eILxpEo5OvuqE6yWkqWBIb5dU0DqhKQtMeny+FBD3PK6lm7L5xQ== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -3232,6 +3278,28 @@ dependencies: "@types/node" "*" +"@types/ssh2-streams@*": + version "0.1.12" + resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz#e68795ba2bf01c76b93f9c9809e1f42f0eaaec5f" + integrity sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg== + dependencies: + "@types/node" "*" + +"@types/ssh2@*": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.15.1.tgz#4db4b6864abca09eb299fe5354fa591add412223" + integrity sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA== + dependencies: + "@types/node" "^18.11.18" + +"@types/ssh2@^0.5.48": + version "0.5.52" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.52.tgz#9dbd8084e2a976e551d5e5e70b978ed8b5965741" + integrity sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg== + dependencies: + "@types/node" "*" + "@types/ssh2-streams" "*" + "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" @@ -3390,7 +3458,7 @@ ts-xor "^1.3.0" vaul "^1.0.0" -"@vector-im/matrix-wysiwyg-wasm@link:../../bindings/wysiwyg-wasm": +"@vector-im/matrix-wysiwyg-wasm@link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.0-af862ffd231dc0a6b8d6f2cb3601e68456c0ff24-integrity/node_modules/bindings/wysiwyg-wasm": version "0.0.0" uid "" @@ -3569,6 +3637,13 @@ abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -3727,6 +3802,32 @@ anymatch@^3.0.3, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +archiver-utils@^5.0.0, archiver-utils@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" + integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA== + dependencies: + glob "^10.0.0" + graceful-fs "^4.2.0" + is-stream "^2.0.1" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +archiver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" + integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ== + dependencies: + archiver-utils "^5.0.2" + async "^3.2.4" + buffer-crc32 "^1.0.0" + readable-stream "^4.0.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^6.0.1" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -3862,6 +3963,13 @@ arraybuffer.prototype.slice@^1.0.3: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + asn1js@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" @@ -3881,7 +3989,12 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async@^3.2.3: +async-lock@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f" + integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ== + +async@^3.2.3, async@^3.2.4: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== @@ -3925,6 +4038,11 @@ axobject-query@^4.1.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== +b4a@^1.6.4: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + babel-jest@^29.0.0, babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -4035,6 +4153,39 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== +bare-events@^2.0.0, bare-events@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" + integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== + +bare-fs@^2.1.1: + version "2.3.5" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.5.tgz#05daa8e8206aeb46d13c2fe25a2cd3797b0d284a" + integrity sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw== + dependencies: + bare-events "^2.0.0" + bare-path "^2.0.0" + bare-stream "^2.0.0" + +bare-os@^2.1.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.4.tgz#01243392eb0a6e947177bb7c8a45123d45c9b1a9" + integrity sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ== + +bare-path@^2.0.0, bare-path@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e" + integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA== + dependencies: + bare-os "^2.1.0" + +bare-stream@^2.0.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.1.tgz#b3b9874fab05b662c9aea2706a12fb0698c46836" + integrity sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g== + dependencies: + streamx "^2.21.0" + base-x@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.0.tgz#6d835ceae379130e1a4cb846a70ac4746f28ea9b" @@ -4045,11 +4196,23 @@ base64-arraybuffer@^1.0.2: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -4060,6 +4223,15 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + blob-polyfill@^9.0.0: version "9.0.20240710" resolved "https://registry.yarnpkg.com/blob-polyfill/-/blob-polyfill-9.0.20240710.tgz#2ef075a207311ea327704f04dc4a98cbefe4143b" @@ -4161,11 +4333,37 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-crc32@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" + integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +buildcheck@~0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" + integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== + builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -4178,6 +4376,11 @@ bundle-name@^4.1.0: dependencies: run-applescript "^7.0.0" +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q== + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -4322,6 +4525,11 @@ chokidar@^4.0.0: dependencies: readdirp "^4.0.1" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chrome-trace-event@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" @@ -4518,6 +4726,17 @@ commonmark@^0.31.0: mdurl "~1.0.1" minimist "~1.2.8" +compress-commons@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" + integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^6.0.0" + is-stream "^2.0.1" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -4659,11 +4878,32 @@ counterpart@^0.18.6: pluralizers "^0.1.7" sprintf-js "^1.0.3" +cpu-features@~0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5" + integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== + dependencies: + buildcheck "~0.0.6" + nan "^2.19.0" + crc-32@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-0.3.0.tgz#6a3d3687f5baec41f7e9b99fe1953a2e5d19775e" integrity sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA== +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" + integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g== + dependencies: + crc-32 "^1.2.0" + readable-stream "^4.0.0" + create-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" @@ -4972,7 +5212,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.3.1: +debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.5: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -5140,6 +5380,32 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docker-compose@^0.24.8: + version "0.24.8" + resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.24.8.tgz#6c125e6b9e04cf68ced47e2596ef2bb93ee9694e" + integrity sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw== + dependencies: + yaml "^2.2.2" + +docker-modem@^3.0.0: + version "3.0.8" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.8.tgz#ef62c8bdff6e8a7d12f0160988c295ea8705e77a" + integrity sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.11.0" + +dockerode@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.5.tgz#7ae3f40f2bec53ae5e9a741ce655fff459745629" + integrity sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA== + dependencies: + "@balena/dockerignore" "^1.0.2" + docker-modem "^3.0.0" + tar-fs "~2.0.1" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -5354,6 +5620,13 @@ encodeurl@~2.0.0: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -5832,6 +6105,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -5842,7 +6120,7 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -events@^3.2.0: +events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -5952,6 +6230,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -6216,6 +6499,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^11.0.0: version "11.2.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" @@ -6320,6 +6608,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -6368,6 +6661,18 @@ glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@^10.0.0: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" @@ -6780,7 +7085,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.12: +ieee754@^1.1.12, ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -6839,7 +7144,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7127,7 +7432,7 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-stream@^2.0.0: +is-stream@^2.0.0, is-stream@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== @@ -7280,6 +7585,15 @@ iterator.prototype@^1.1.3: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jackspeak@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015" @@ -7940,6 +8254,13 @@ launch-editor@^2.6.1: picocolors "^1.0.0" shell-quote "^1.8.1" +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -8095,7 +8416,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -8439,7 +8760,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -8475,6 +8796,11 @@ minipass@^4.2.4: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -8535,6 +8861,11 @@ murmurhash-js@^1.0.0: resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== +nan@^2.19.0, nan@^2.20.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" + integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== + nanoid@^3.3.7: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" @@ -8717,7 +9048,7 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -8953,7 +9284,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.6.1: +path-scurry@^1.11.1, path-scurry@^1.6.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -9780,6 +10111,22 @@ prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proper-lockfile@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + +properties-reader@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/properties-reader/-/properties-reader-2.3.0.tgz#f3ab84224c9535a7a36e011ae489a79a13b472b2" + integrity sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw== + dependencies: + mkdirp "^1.0.4" + protocol-buffers-schema@^3.3.1: version "3.6.0" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" @@ -9803,6 +10150,14 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +pump@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0, punycode@^2.1.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -9856,6 +10211,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + quickselect@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" @@ -10049,7 +10409,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.1, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -10062,7 +10422,7 @@ readable-stream@^2.0.1, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -10071,6 +10431,24 @@ readable-stream@^3.0.6: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.6.0.tgz#ce412dfb19c04efde1c5936d99c27f37a1ff94c9" + integrity sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + readdirp@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" @@ -10289,6 +10667,11 @@ restore-cursor@^5.0.0: onetime "^7.0.0" signal-exit "^4.1.0" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -10377,7 +10760,7 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -10597,7 +10980,7 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" -signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -10761,6 +11144,11 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== + sprintf-js@^1.0.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" @@ -10771,6 +11159,25 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +ssh-remote-port-forward@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz#72b0c5df8ec27ca300c75805cc6b266dee07e298" + integrity sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ== + dependencies: + "@types/ssh2" "^0.5.48" + ssh2 "^1.4.0" + +ssh2@^1.11.0, ssh2@^1.4.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.16.0.tgz#79221d40cbf4d03d07fe881149de0a9de928c9f0" + integrity sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.10" + nan "^2.20.0" + stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -10788,6 +11195,17 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +streamx@^2.15.0, streamx@^2.21.0: + version "2.21.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.21.1.tgz#f02979d8395b6b637d08a589fb514498bed55845" + integrity sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + string-argv@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -10900,7 +11318,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -11158,11 +11576,52 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-fs@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217" + integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w== + dependencies: + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^2.1.1" + bare-path "^2.1.0" + +tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + tar-js@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/tar-js/-/tar-js-0.3.0.tgz#6949aabfb0ba18bb1562ae51a439fd0f30183a17" integrity sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA== +tar-stream@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^3.0.0, tar-stream@^3.1.5: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + temporal-polyfill@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/temporal-polyfill/-/temporal-polyfill-0.2.5.tgz#0796c40a50754c69ec0f9a2db3f6c582b9721aaf" @@ -11215,6 +11674,34 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +testcontainers@^10.16.0: + version "10.16.0" + resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.16.0.tgz#8a7e69ada5cd2c6cce1c6db72b3a3e8e412fcaf6" + integrity sha512-oxPLuOtrRWS11A+Yn0+zXB7GkmNarflWqmy6CQJk8KJ75LZs2/zlUXDpizTbPpCGtk4kE2EQYwFZjrE967F8Wg== + dependencies: + "@balena/dockerignore" "^1.0.2" + "@types/dockerode" "^3.3.29" + archiver "^7.0.1" + async-lock "^1.4.1" + byline "^5.0.0" + debug "^4.3.5" + docker-compose "^0.24.8" + dockerode "^3.3.5" + get-port "^5.1.1" + proper-lockfile "^4.1.2" + properties-reader "^2.3.0" + ssh-remote-port-forward "^1.0.4" + tar-fs "^3.0.6" + tmp "^0.2.3" + undici "^5.28.4" + +text-decoder@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" + integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== + dependencies: + b4a "^1.6.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -11248,6 +11735,11 @@ tinyqueue@^3.0.0: resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-3.0.0.tgz#101ea761ccc81f979e29200929e78f1556e3661e" integrity sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g== +tmp@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -11395,6 +11887,11 @@ tslib@^2.0.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2, tslib@^2.7.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -11520,6 +12017,13 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici@^5.28.4: + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== + dependencies: + "@fastify/busboy" "^2.0.0" + unhomoglyph@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3" @@ -12174,6 +12678,11 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.2.2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" + integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== + yaml@^2.3.3: version "2.6.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" @@ -12242,6 +12751,15 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== +zip-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb" + integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA== + dependencies: + archiver-utils "^5.0.0" + compress-commons "^6.0.2" + readable-stream "^4.0.0" + zod-validation-error@^3.0.3: version "3.4.0" resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6" From 87a0acb53aa6ba1f9ccb203b2fc62b9df949a5dc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 3 Jan 2025 18:54:31 +0000 Subject: [PATCH 02/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 2 -- playwright/e2e/login/utils.ts | 3 +- playwright/plugins/utils/object.ts | 16 +++++++++++ playwright/testcontainers/dendrite.ts | 2 +- playwright/testcontainers/mas.ts | 6 ++-- playwright/testcontainers/synapse.ts | 2 +- yarn.lock | 40 +-------------------------- 7 files changed, 22 insertions(+), 49 deletions(-) create mode 100644 playwright/plugins/utils/object.ts diff --git a/package.json b/package.json index 846a8ca6d9b..b629e8e08e6 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,6 @@ "@types/escape-html": "^1.0.1", "@types/express": "^5.0.0", "@types/file-saver": "^2.0.3", - "@types/fs-extra": "^11.0.0", "@types/glob-to-regexp": "^0.4.1", "@types/jest": "29.5.12", "@types/jitsi-meet": "^2.0.2", @@ -242,7 +241,6 @@ "fetch-mock": "9.11.0", "fetch-mock-jest": "^1.5.1", "file-loader": "^6.0.0", - "fs-extra": "^11.0.0", "glob": "^11.0.0", "html-webpack-plugin": "^5.5.3", "husky": "^9.0.0", diff --git a/playwright/e2e/login/utils.ts b/playwright/e2e/login/utils.ts index ae794bd9119..dffa6ad7be0 100644 --- a/playwright/e2e/login/utils.ts +++ b/playwright/e2e/login/utils.ts @@ -6,10 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Page, expect, Fixtures } from "@playwright/test"; +import { Page, expect } from "@playwright/test"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; -import { Services } from "../../services.ts"; /** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element */ diff --git a/playwright/plugins/utils/object.ts b/playwright/plugins/utils/object.ts new file mode 100644 index 00000000000..bfb92fecec2 --- /dev/null +++ b/playwright/plugins/utils/object.ts @@ -0,0 +1,16 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +/** + * Deep copy the given object. The object MUST NOT have circular references and + * MUST NOT have functions. + * @param obj - The object to deep copy. + * @returns A copy of the object without any references to the original. + */ +export function deepCopy(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index e073fbcada9..006e7d6b946 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details. import { GenericContainer, Wait } from "testcontainers"; import { APIRequestContext } from "@playwright/test"; import * as YAML from "yaml"; -import { deepCopy } from "matrix-js-sdk/src/utils"; import { set } from "lodash"; import { getFreePort } from "../plugins/utils/port.ts"; import { randB64Bytes } from "../plugins/utils/rand.ts"; import { StartedSynapseContainer } from "./synapse.ts"; +import { deepCopy } from "../plugins/utils/object.ts"; const DEFAULT_CONFIG = { version: 2, diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index b4e5bf75377..f2cdef6794b 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -5,13 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; -import crypto from "node:crypto"; +import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"; import * as YAML from "yaml"; import { getFreePort } from "../plugins/utils/port.ts"; -import { Credentials } from "../plugins/homeserver"; -import { deepCopy } from "matrix-js-sdk/src/utils.ts"; +import { deepCopy } from "../plugins/utils/object.ts"; const DEFAULT_CONFIG = { http: { diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index c3cb0ab69fa..33a42c1a68e 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -9,12 +9,12 @@ import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait import { APIRequestContext } from "@playwright/test"; import crypto from "node:crypto"; import * as YAML from "yaml"; -import { deepCopy } from "matrix-js-sdk/src/utils"; import { set } from "lodash"; import { getFreePort } from "../plugins/utils/port.ts"; import { randB64Bytes } from "../plugins/utils/rand.ts"; import { Credentials, HomeserverInstance } from "../plugins/homeserver"; +import { deepCopy } from "../plugins/utils/object.ts"; const TAG = "develop@sha256:17cc0a301447430624afb860276e5c13270ddeb99a3f6d1c6d519a20b1a8f650"; diff --git a/yarn.lock b/yarn.lock index a68b8cf97ae..6697ad9d331 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2939,14 +2939,6 @@ resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d" integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A== -"@types/fs-extra@^11.0.0": - version "11.0.4" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45" - integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== - dependencies: - "@types/jsonfile" "*" - "@types/node" "*" - "@types/geojson-vt@3.2.5": version "3.2.5" resolved "https://registry.yarnpkg.com/@types/geojson-vt/-/geojson-vt-3.2.5.tgz#b6c356874991d9ab4207533476dfbcdb21e38408" @@ -3047,13 +3039,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonfile@*": - version "6.1.4" - resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702" - integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== - dependencies: - "@types/node" "*" - "@types/jsrsasign@^10.5.4": version "10.5.15" resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-10.5.15.tgz#5cf1ee506b2fa2435b6e1786a873285c7110eb82" @@ -6504,15 +6489,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^11.0.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -6798,7 +6774,7 @@ gopd@^1.1.0: dependencies: get-intrinsic "^1.2.4" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -8129,15 +8105,6 @@ json5@^2.1.2, json5@^2.1.3, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - jsqr@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.4.0.tgz#8efb8d0a7cc6863cb6d95116b9069123ce9eb2d1" @@ -12062,11 +12029,6 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" From 7ba1d3e14ca59e3f735808cdde1f802f29cf0368 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 3 Jan 2025 21:10:36 +0000 Subject: [PATCH 03/93] Flip fixture dependency order Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/services.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright/services.ts b/playwright/services.ts index bf4bad18fcc..2ebade221e1 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -78,7 +78,7 @@ export const test = base.extend({ const container = new SynapseContainer(request); await use(container); }, - homeserver: async ({ network, _homeserver: homeserver, synapseConfigOptions }, use) => { + homeserver: async ({ mas, network, _homeserver: homeserver, synapseConfigOptions }, use) => { const container = await homeserver .withNetwork(network) .withNetworkAliases("homeserver") @@ -88,7 +88,7 @@ export const test = base.extend({ await use(container); await container.stop(); }, - mas: async ({ network, homeserver }, use) => { + mas: async ({ network }, use) => { const container = await new MatrixAuthenticationServiceContainer() .withNetwork(network) .withNetworkAliases("mas") From c0337f141bc4c44d83e92025ef2f8e0da3f973d1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 08:34:19 +0000 Subject: [PATCH 04/93] Remove mas dep Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/services.ts b/playwright/services.ts index 2ebade221e1..e85fd45ccfd 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -78,7 +78,7 @@ export const test = base.extend({ const container = new SynapseContainer(request); await use(container); }, - homeserver: async ({ mas, network, _homeserver: homeserver, synapseConfigOptions }, use) => { + homeserver: async ({ network, _homeserver: homeserver, synapseConfigOptions }, use) => { const container = await homeserver .withNetwork(network) .withNetworkAliases("homeserver") From a4ba79ae695849d279f7b75c02f0e4e3905f3b0d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 08:58:11 +0000 Subject: [PATCH 05/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/sliding-sync/sliding-sync.spec.ts | 3 +- playwright/services.ts | 22 +++++++++--- playwright/testcontainers/synapse.ts | 2 +- playwright/testcontainers/utils.ts | 36 +++++++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 playwright/testcontainers/utils.ts diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index d105904a483..0ddfabc549f 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -18,10 +18,11 @@ const test = base.extend<{ testRoom: { roomId: string; name: string }; joinedBot: Bot; }>({ - slidingSyncProxy: async ({ network, postgres, page, homeserver }, use, testInfo) => { + slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => { const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3") .withNetwork(network) .withExposedPorts(8008) + .withLogConsumer(logger.getConsumer("sliding-sync-proxy")) .withEnvironment({ SYNCV3_SECRET: "bwahahaha", SYNCV3_DB: `user=postgres dbname=postgres password=${postgres.getPassword()} host=${postgres.getHost()} sslmode=disable`, diff --git a/playwright/services.ts b/playwright/services.ts index e85fd45ccfd..b1ab5c6a98b 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -12,8 +12,11 @@ import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers import { StartedSynapseContainer, SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts"; import { MatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; +import { ContainerLogger } from "./testcontainers/utils.ts"; export interface Services { + logger: ContainerLogger; + network: StartedNetwork; postgres: StartedPostgreSqlContainer; @@ -26,18 +29,24 @@ export interface Services { mas: StartedTestContainer; } -// TODO logs export const test = base.extend({ + // eslint-disable-next-line no-empty-pattern + logger: async ({}, use, testInfo) => { + const logger = new ContainerLogger(); + await use(logger); + await logger.testFinished(testInfo); + }, // eslint-disable-next-line no-empty-pattern network: async ({}, use) => { const network = await new Network().start(); await use(network); await network.stop(); }, - postgres: async ({ network }, use) => { + postgres: async ({ logger, network }, use) => { const container = await new PostgreSqlContainer() .withNetwork(network) .withNetworkAliases("postgres") + .withLogConsumer(logger.getConsumer("postgres")) .withTmpFs({ "/dev/shm/pgdata/data": "", }) @@ -59,11 +68,12 @@ export const test = base.extend({ await container.stop(); }, - mailhog: async ({ network }, use) => { + mailhog: async ({ logger, network }, use) => { const container = await new GenericContainer("mailhog/mailhog:latest") .withNetwork(network) .withNetworkAliases("mailhog") .withExposedPorts(8025) + .withLogConsumer(logger.getConsumer("mailhog")) .withWaitStrategy(Wait.forListeningPorts()) .start(); await use(container); @@ -78,20 +88,22 @@ export const test = base.extend({ const container = new SynapseContainer(request); await use(container); }, - homeserver: async ({ network, _homeserver: homeserver, synapseConfigOptions }, use) => { + homeserver: async ({ logger, network, _homeserver: homeserver, synapseConfigOptions }, use) => { const container = await homeserver .withNetwork(network) .withNetworkAliases("homeserver") + .withLogConsumer(logger.getConsumer("synapse")) .withConfig(synapseConfigOptions) .start(); await use(container); await container.stop(); }, - mas: async ({ network }, use) => { + mas: async ({ logger, network }, use) => { const container = await new MatrixAuthenticationServiceContainer() .withNetwork(network) .withNetworkAliases("mas") + .withLogConsumer(logger.getConsumer("mas")) .withConfig({ clients: [ { diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 33a42c1a68e..0e70e0d960c 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -94,7 +94,7 @@ const DEFAULT_CONFIG = { burst_count: 10000, }, }, - media_store_path: "/media", + media_store_path: "/data/media_store", max_upload_size: "50M", max_image_pixels: "32M", dynamic_thumbnails: false, diff --git a/playwright/testcontainers/utils.ts b/playwright/testcontainers/utils.ts new file mode 100644 index 00000000000..58afcce138a --- /dev/null +++ b/playwright/testcontainers/utils.ts @@ -0,0 +1,36 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { TestInfo } from "@playwright/test"; +import { Readable } from "stream"; + +export class ContainerLogger { + private logs: Record = {}; + + public getConsumer(container: string) { + this.logs[container] = ""; + return (stream: Readable) => { + stream.on("data", (chunk) => { + this.logs[container] += chunk.toString(); + }); + stream.on("err", (chunk) => { + this.logs[container] += "ERR " + chunk.toString(); + }); + }; + } + + public async testFinished(testInfo: TestInfo) { + if (testInfo.status !== "passed") { + for (const container in this.logs) { + await testInfo.attach(container, { + body: this.logs[container], + contentType: "text/plain", + }); + } + } + } +} From 94697fe755f24903056038f2fc6e2b4ced3613b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 09:34:31 +0000 Subject: [PATCH 06/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/dendrite.ts | 19 ++++--------------- playwright/testcontainers/synapse.ts | 2 ++ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index 006e7d6b946..25535ae475b 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -211,30 +211,19 @@ export class DendriteContainer extends GenericContainer { constructor( private request: APIRequestContext, image = "matrixdotorg/dendrite-monolith:main", - entrypoint = "/usr/bin/dendrite", + binary = "/usr/bin/dendrite", ) { super(image); this.config = deepCopy(DEFAULT_CONFIG); this.config.client_api.registration_shared_secret = randB64Bytes(16); - this.withEntrypoint([entrypoint]) + this.withEntrypoint(["/bin/sh"]) .withCommand([ - "--config", - "/etc/dendrite/dendrite.yaml", - "--really-enable-open-registration", - "true", - "run", + "-c", + `/usr/bin/generate-keys -private-key /etc/dendrite/matrix_key.pem && ${binary} --config /etc/dendrite/dendrite.yaml --really-enable-open-registration true run`, ]) .withWaitStrategy(Wait.forHttp("/_matrix/client/versions", 8008)); - - // const docker = new Docker(); - // await docker.run({ - // image: dendriteImage, - // params: ["--entrypoint=", "-v", `${tempDir}:/mnt`], - // containerName: `react-sdk-playwright-dendrite-keygen`, - // cmd: ["/usr/bin/generate-keys", "-private-key", "/mnt/matrix_key.pem"], - // }); } public withConfigField(key: string, value: any): this { diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 0e70e0d960c..a79166c358f 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -144,6 +144,8 @@ export class SynapseContainer extends GenericContainer { const signingKey = randB64Bytes(32); this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([ + // Create an empty file with permissive permissions to allow the container to write to it + { target: "/data/media_store/.gitkeep", content: "", mode: 0o777 }, { target: "/data/localhost.signing.key", content: `ed25519 x ${signingKey}` }, { target: "/data/log.config", From 9c52986d76babeb1188f28bf5281004bf910c867 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 10:07:34 +0000 Subject: [PATCH 07/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/sliding-sync/sliding-sync.spec.ts | 4 ++-- playwright/testcontainers/synapse.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 0ddfabc549f..08fed9b3bf5 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -25,8 +25,8 @@ const test = base.extend<{ .withLogConsumer(logger.getConsumer("sliding-sync-proxy")) .withEnvironment({ SYNCV3_SECRET: "bwahahaha", - SYNCV3_DB: `user=postgres dbname=postgres password=${postgres.getPassword()} host=${postgres.getHost()} sslmode=disable`, - SYNCV3_SERVER: `http://${homeserver.getNetworkNames()[0]}:8008`, + SYNCV3_DB: `user=postgres dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`, + SYNCV3_SERVER: `http://homeserver:8008`, }) .start(); diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index a79166c358f..45d544ef27c 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -94,7 +94,7 @@ const DEFAULT_CONFIG = { burst_count: 10000, }, }, - media_store_path: "/data/media_store", + media_store_path: "/tmp/media_store", max_upload_size: "50M", max_image_pixels: "32M", dynamic_thumbnails: false, @@ -144,8 +144,6 @@ export class SynapseContainer extends GenericContainer { const signingKey = randB64Bytes(32); this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([ - // Create an empty file with permissive permissions to allow the container to write to it - { target: "/data/media_store/.gitkeep", content: "", mode: 0o777 }, { target: "/data/localhost.signing.key", content: `ed25519 x ${signingKey}` }, { target: "/data/log.config", From f1764732121dfa46c3d5f285adf9d7f8b7a18fbe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 10:45:52 +0000 Subject: [PATCH 08/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/oidc/index.ts | 150 --------------- .../e2e/sliding-sync/sliding-sync.spec.ts | 5 +- playwright/services.ts | 182 ++++++++++++++++-- playwright/testcontainers/mas.ts | 12 +- 4 files changed, 178 insertions(+), 171 deletions(-) diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 0ed405df143..bc83f8fb632 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -12,156 +12,6 @@ import { Page } from "@playwright/test"; import { test as base, expect } from "../../element-web-test"; export const test = base.extend<{}>({ - synapseConfigOptions: async ({ mas }, use) => { - await use({ - enable_registration: undefined, - enable_registration_without_verification: undefined, - disable_msisdn_registration: undefined, - experimental_features: { - msc3861: { - enabled: true, - issuer: "http://mas:8080/", - issuer_metadata: { - "issuer": `http://localhost:${mas.getMappedPort(8080)}/`, - "authorization_endpoint": "http://mas:8080/authorize", - "token_endpoint": "http://mas:8080/oauth2/token", - "jwks_uri": "http://mas:8080/oauth2/keys.json", - "registration_endpoint": "http://mas:8080/oauth2/registration", - "scopes_supported": ["openid", "email"], - "response_types_supported": ["code", "id_token", "code id_token"], - "response_modes_supported": ["form_post", "query", "fragment"], - "grant_types_supported": [ - "authorization_code", - "refresh_token", - "client_credentials", - "urn:ietf:params:oauth:grant-type:device_code", - ], - "token_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "token_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "revocation_endpoint": "http://mas:8080/oauth2/revoke", - "revocation_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "revocation_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "introspection_endpoint": "http://mas:8080/oauth2/introspect", - "introspection_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "introspection_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "code_challenge_methods_supported": ["plain", "S256"], - "userinfo_endpoint": "http://mas:8080/oauth2/userinfo", - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": [ - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "ES256K", - ], - "userinfo_signing_alg_values_supported": [ - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "ES256K", - ], - "display_values_supported": ["page"], - "claim_types_supported": ["normal"], - "claims_supported": [ - "iss", - "sub", - "aud", - "iat", - "exp", - "nonce", - "auth_time", - "at_hash", - "c_hash", - ], - "claims_parameter_supported": false, - "request_parameter_supported": false, - "request_uri_parameter_supported": false, - "prompt_values_supported": ["none", "login", "create"], - "device_authorization_endpoint": "http://mas:8080/oauth2/device", - "org.matrix.matrix-authentication-service.graphql_endpoint": "http://mas:8080/graphql", - "account_management_uri": "http://mas:8080/account/", - "account_management_actions_supported": [ - "org.matrix.profile", - "org.matrix.sessions_list", - "org.matrix.session_view", - "org.matrix.session_end", - ], - }, - client_id: "0000000000000000000SYNAPSE", - client_auth_method: "client_secret_basic", - client_secret: "SomeRandomSecret", - admin_token: "AnotherRandomSecret", - account_management_url: `http://localhost:${mas.getMappedPort(8080)}/account`, - }, - }, - }); - }, config: async ({ homeserver, mas, context }, use) => { const issuer = `http://localhost:${mas.getMappedPort(8080)}/`; const wellKnown = { diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 08fed9b3bf5..f0b717b9724 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { Page, Request } from "@playwright/test"; -import { GenericContainer, StartedTestContainer } from "testcontainers"; +import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"; import { test as base, expect } from "../../element-web-test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; @@ -23,9 +23,10 @@ const test = base.extend<{ .withNetwork(network) .withExposedPorts(8008) .withLogConsumer(logger.getConsumer("sliding-sync-proxy")) + .withWaitStrategy(Wait.forHttp("/client/server.json", 8008)) .withEnvironment({ SYNCV3_SECRET: "bwahahaha", - SYNCV3_DB: `user=postgres dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`, + SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`, SYNCV3_SERVER: `http://homeserver:8008`, }) .start(); diff --git a/playwright/services.ts b/playwright/services.ts index b1ab5c6a98b..e9b735c3ab1 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -99,26 +99,178 @@ export const test = base.extend({ await use(container); await container.stop(); }, - mas: async ({ logger, network }, use) => { - const container = await new MatrixAuthenticationServiceContainer() + mas: async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => { + const config = { + clients: [ + { + client_id: "0000000000000000000SYNAPSE", + client_auth_method: "client_secret_basic", + client_secret: "SomeRandomSecret", + }, + ], + matrix: { + homeserver: "localhost", + secret: "AnotherRandomSecret", + endpoint: "http://synapse:8008", + }, + }; + + const container = await new MatrixAuthenticationServiceContainer(postgres) .withNetwork(network) .withNetworkAliases("mas") .withLogConsumer(logger.getConsumer("mas")) - .withConfig({ - clients: [ - { - client_id: "0000000000000000000SYNAPSE", - client_auth_method: "client_secret_basic", - client_secret: "SomeRandomSecret", + .withConfig(config) + .start(); + + homeserver.withConfig({ + enable_registration: undefined, + enable_registration_without_verification: undefined, + disable_msisdn_registration: undefined, + experimental_features: { + msc3861: { + enabled: true, + issuer: "http://mas:8080/", + issuer_metadata: { + "issuer": `http://${container.getHost()}:${container.getMappedPort(8080)}/`, + "authorization_endpoint": "http://mas:8080/authorize", + "token_endpoint": "http://mas:8080/oauth2/token", + "jwks_uri": "http://mas:8080/oauth2/keys.json", + "registration_endpoint": "http://mas:8080/oauth2/registration", + "scopes_supported": ["openid", "email"], + "response_types_supported": ["code", "id_token", "code id_token"], + "response_modes_supported": ["form_post", "query", "fragment"], + "grant_types_supported": [ + "authorization_code", + "refresh_token", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "revocation_endpoint": "http://mas:8080/oauth2/revoke", + "revocation_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "revocation_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "introspection_endpoint": "http://mas:8080/oauth2/introspect", + "introspection_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "introspection_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "code_challenge_methods_supported": ["plain", "S256"], + "userinfo_endpoint": "http://mas:8080/oauth2/userinfo", + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "ES256K", + ], + "userinfo_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "ES256K", + ], + "display_values_supported": ["page"], + "claim_types_supported": ["normal"], + "claims_supported": [ + "iss", + "sub", + "aud", + "iat", + "exp", + "nonce", + "auth_time", + "at_hash", + "c_hash", + ], + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": false, + "prompt_values_supported": ["none", "login", "create"], + "device_authorization_endpoint": "http://mas:8080/oauth2/device", + "org.matrix.matrix-authentication-service.graphql_endpoint": "http://mas:8080/graphql", + "account_management_uri": "http://mas:8080/account/", + "account_management_actions_supported": [ + "org.matrix.profile", + "org.matrix.sessions_list", + "org.matrix.session_view", + "org.matrix.session_end", + ], }, - ], - matrix: { - homeserver: "localhost", - secret: "AnotherRandomSecret", - endpoint: "http://synapse:8008", + client_id: config.clients[0].client_id, + client_auth_method: config.clients[0].client_auth_method, + client_secret: config.clients[0].client_secret, + admin_token: config.matrix.secret, + account_management_url: `http://${container.getHost()}:${container.getMappedPort(8080)}/account`, }, - }) - .start(); + }, + }); + await use(container); await container.stop(); }, diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index f2cdef6794b..833167b141e 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { StartedPostgreSqlContainer } from "@testcontainers/postgresql"; import * as YAML from "yaml"; import { getFreePort } from "../plugins/utils/port.ts"; @@ -54,8 +55,7 @@ const DEFAULT_CONFIG = { ], binds: [ { - host: "localhost", - port: 8081, + address: "[::]:8081", }, ], proxy_protocol: false, @@ -167,12 +167,16 @@ const DEFAULT_CONFIG = { export class MatrixAuthenticationServiceContainer extends GenericContainer { private config: typeof DEFAULT_CONFIG; - constructor() { + constructor(db: StartedPostgreSqlContainer) { super("ghcr.io/matrix-org/matrix-authentication-service:0.8.0"); this.config = deepCopy(DEFAULT_CONFIG); + this.config.database.username = db.getUsername(); + this.config.database.password = db.getPassword(); - this.withWaitStrategy(Wait.forHttp("/health", 8081)).withCommand(["server", "--config", "/config/config.yaml"]); + this.withExposedPorts(8080, 8081) + .withWaitStrategy(Wait.forHttp("/health", 8081)) + .withCommand(["server", "--config", "/config/config.yaml"]); } public withConfig(config: object): this { From 75ba4f2b2ad72694c32d29e3a0f39d2df64c7bff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 11:51:37 +0000 Subject: [PATCH 09/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups.spec.ts | 12 +- playwright/e2e/oidc/index.ts | 28 +-- playwright/e2e/oidc/oidc-native.spec.ts | 9 +- .../e2e/sliding-sync/sliding-sync.spec.ts | 2 +- playwright/element-web-test.ts | 10 +- .../homeserver/synapse/masHomeserver.ts | 191 ++++++++++++++++++ playwright/services.ts | 184 +---------------- 7 files changed, 221 insertions(+), 215 deletions(-) create mode 100644 playwright/plugins/homeserver/synapse/masHomeserver.ts diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index 8826cb4595c..feeaf961df8 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -9,8 +9,9 @@ Please see LICENSE files in the repository root for full details. import { type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; -import { test as masTest, registerAccountMas } from "../oidc"; +import { registerAccountMas } from "../oidc"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; async function expectBackupVersionToBe(page: Page, version: string) { await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText( @@ -20,10 +21,11 @@ async function expectBackupVersionToBe(page: Page, version: string) { await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version); } -masTest.describe("Encryption state after registration", () => { - masTest.skip(isDendrite, "does not yet support MAS"); +test.describe("Encryption state after registration", () => { + test.use(masHomeserver); + test.skip(isDendrite, "does not yet support MAS"); - masTest("Key backup is enabled by default", async ({ page, mailhogClient, app }) => { + test("Key backup is enabled by default", async ({ page, mailhogClient, app }) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); @@ -32,7 +34,7 @@ masTest.describe("Encryption state after registration", () => { expect(page.getByText("This session is backing up your keys.")).toBeVisible(); }); - masTest("user is prompted to set up recovery", async ({ page, mailhogClient, app }) => { + test("user is prompted to set up recovery", async ({ page, mailhogClient, app }) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index bc83f8fb632..8ea03ff37a7 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -9,33 +9,7 @@ Please see LICENSE files in the repository root for full details. import { API, Messages } from "mailhog"; import { Page } from "@playwright/test"; -import { test as base, expect } from "../../element-web-test"; - -export const test = base.extend<{}>({ - config: async ({ homeserver, mas, context }, use) => { - const issuer = `http://localhost:${mas.getMappedPort(8080)}/`; - const wellKnown = { - "m.homeserver": { - base_url: homeserver.baseUrl, - }, - "org.matrix.msc2965.authentication": { - issuer, - account: `${issuer}account`, - }, - }; - - // Ensure org.matrix.msc2965.authentication is in well-known - await context.route("https://localhost/.well-known/matrix/client", async (route) => { - await route.fulfill({ json: wellKnown }); - }); - - await use({ - default_server_config: wellKnown, - }); - }, -}); - -export { expect }; +import { expect } from "../../element-web-test"; export async function registerAccountMas( page: Page, diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 78696d75ef3..e5bb7e2ce64 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -6,16 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { test, expect, registerAccountMas } from "."; +import { test, expect } from "../../element-web-test.ts"; +import { registerAccountMas } from "."; import { ElementAppPage } from "../../pages/ElementAppPage.ts"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { + test.use(masHomeserver); test.skip(isDendrite, "does not yet support MAS"); test.slow(); // trace recording takes a while here test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => { - const tokenUri = `http://localhost:${mas.getMappedPort(8080)}/oauth2/token`; + const tokenUri = `http://${mas.getHost()}:${mas.getMappedPort(8080)}/oauth2/token`; const tokenApiPromise = page.waitForRequest( (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", ); @@ -49,7 +52,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { await newPage.close(); // Assert logging out revokes both tokens - const revokeUri = `http://localhost:${mas.getMappedPort(8080)}/oauth2/revoke`; + const revokeUri = `http://${mas.getHost()}:${mas.getMappedPort(8080)}/oauth2/revoke`; const revokeAccessTokenPromise = page.waitForRequest( (request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token", ); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index f0b717b9724..dec588c5b2f 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -31,7 +31,7 @@ const test = base.extend<{ }) .start(); - const proxyAddress = `http://localhost:${container.getMappedPort(8008)}`; + const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`; await page.addInitScript((proxyAddress) => { window.localStorage.setItem( "mx_local_settings", diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index e53ebc0818c..ed1bdb2b6ca 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -165,8 +165,14 @@ export const test = base.extend({ window.localStorage.setItem("mx_has_pickle_key", "false"); window.localStorage.setItem("mx_has_access_token", "true"); - // Ensure the language is set to a consistent value - window.localStorage.setItem("mx_local_settings", '{"language":"en"}'); + window.localStorage.setItem( + "mx_local_settings", + JSON.stringify({ + ...JSON.parse(window.localStorage.getItem("mx_local_settings") || "{}"), + // Ensure the language is set to a consistent value + language: "en", + }), + ); }, { baseUrl: homeserver.baseUrl, credentials }, ); diff --git a/playwright/plugins/homeserver/synapse/masHomeserver.ts b/playwright/plugins/homeserver/synapse/masHomeserver.ts new file mode 100644 index 00000000000..a9670eb60e6 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/masHomeserver.ts @@ -0,0 +1,191 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2023 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { Fixtures } from "@playwright/test"; + +import { Services } from "../../../services.ts"; +import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts"; + +export const masHomeserver: Fixtures = { + mas: async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => { + const config = { + clients: [ + { + client_id: "0000000000000000000SYNAPSE", + client_auth_method: "client_secret_basic", + client_secret: "SomeRandomSecret", + }, + ], + matrix: { + homeserver: "localhost", + secret: "AnotherRandomSecret", + endpoint: "http://synapse:8008", + }, + }; + + const container = await new MatrixAuthenticationServiceContainer(postgres) + .withNetwork(network) + .withNetworkAliases("mas") + .withLogConsumer(logger.getConsumer("mas")) + .withConfig(config) + .start(); + + homeserver.withConfig({ + enable_registration: undefined, + enable_registration_without_verification: undefined, + disable_msisdn_registration: undefined, + password_config: undefined, + experimental_features: { + msc3861: { + enabled: true, + issuer: "http://mas:8080/", + issuer_metadata: { + "issuer": `http://${container.getHost()}:${container.getMappedPort(8080)}/`, + "authorization_endpoint": "http://mas:8080/authorize", + "token_endpoint": "http://mas:8080/oauth2/token", + "jwks_uri": "http://mas:8080/oauth2/keys.json", + "registration_endpoint": "http://mas:8080/oauth2/registration", + "scopes_supported": ["openid", "email"], + "response_types_supported": ["code", "id_token", "code id_token"], + "response_modes_supported": ["form_post", "query", "fragment"], + "grant_types_supported": [ + "authorization_code", + "refresh_token", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "revocation_endpoint": "http://mas:8080/oauth2/revoke", + "revocation_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "revocation_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "introspection_endpoint": "http://mas:8080/oauth2/introspect", + "introspection_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none", + ], + "introspection_endpoint_auth_signing_alg_values_supported": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "code_challenge_methods_supported": ["plain", "S256"], + "userinfo_endpoint": "http://mas:8080/oauth2/userinfo", + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "ES256K", + ], + "userinfo_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "ES256K", + ], + "display_values_supported": ["page"], + "claim_types_supported": ["normal"], + "claims_supported": [ + "iss", + "sub", + "aud", + "iat", + "exp", + "nonce", + "auth_time", + "at_hash", + "c_hash", + ], + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": false, + "prompt_values_supported": ["none", "login", "create"], + "device_authorization_endpoint": "http://mas:8080/oauth2/device", + "org.matrix.matrix-authentication-service.graphql_endpoint": "http://mas:8080/graphql", + "account_management_uri": "http://mas:8080/account/", + "account_management_actions_supported": [ + "org.matrix.profile", + "org.matrix.sessions_list", + "org.matrix.session_view", + "org.matrix.session_end", + ], + }, + client_id: config.clients[0].client_id, + client_auth_method: config.clients[0].client_auth_method, + client_secret: config.clients[0].client_secret, + admin_token: config.matrix.secret, + account_management_url: `http://${container.getHost()}:${container.getMappedPort(8080)}/account`, + }, + }, + }); + + await use(container); + await container.stop(); + }, +}; diff --git a/playwright/services.ts b/playwright/services.ts index e9b735c3ab1..770b30b8fca 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -11,7 +11,6 @@ import { GenericContainer, Network, StartedNetwork, StartedTestContainer, Wait } import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql"; import { StartedSynapseContainer, SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts"; -import { MatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; import { ContainerLogger } from "./testcontainers/utils.ts"; export interface Services { @@ -26,7 +25,7 @@ export interface Services { synapseConfigOptions: SynapseConfigOptions; _homeserver: SynapseContainer; homeserver: StartedSynapseContainer; - mas: StartedTestContainer; + mas?: StartedTestContainer; } export const test = base.extend({ @@ -88,7 +87,7 @@ export const test = base.extend({ const container = new SynapseContainer(request); await use(container); }, - homeserver: async ({ logger, network, _homeserver: homeserver, synapseConfigOptions }, use) => { + homeserver: async ({ logger, network, _homeserver: homeserver, synapseConfigOptions, mas }, use) => { const container = await homeserver .withNetwork(network) .withNetworkAliases("homeserver") @@ -99,179 +98,10 @@ export const test = base.extend({ await use(container); await container.stop(); }, - mas: async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => { - const config = { - clients: [ - { - client_id: "0000000000000000000SYNAPSE", - client_auth_method: "client_secret_basic", - client_secret: "SomeRandomSecret", - }, - ], - matrix: { - homeserver: "localhost", - secret: "AnotherRandomSecret", - endpoint: "http://synapse:8008", - }, - }; - - const container = await new MatrixAuthenticationServiceContainer(postgres) - .withNetwork(network) - .withNetworkAliases("mas") - .withLogConsumer(logger.getConsumer("mas")) - .withConfig(config) - .start(); - - homeserver.withConfig({ - enable_registration: undefined, - enable_registration_without_verification: undefined, - disable_msisdn_registration: undefined, - experimental_features: { - msc3861: { - enabled: true, - issuer: "http://mas:8080/", - issuer_metadata: { - "issuer": `http://${container.getHost()}:${container.getMappedPort(8080)}/`, - "authorization_endpoint": "http://mas:8080/authorize", - "token_endpoint": "http://mas:8080/oauth2/token", - "jwks_uri": "http://mas:8080/oauth2/keys.json", - "registration_endpoint": "http://mas:8080/oauth2/registration", - "scopes_supported": ["openid", "email"], - "response_types_supported": ["code", "id_token", "code id_token"], - "response_modes_supported": ["form_post", "query", "fragment"], - "grant_types_supported": [ - "authorization_code", - "refresh_token", - "client_credentials", - "urn:ietf:params:oauth:grant-type:device_code", - ], - "token_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "token_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "revocation_endpoint": "http://mas:8080/oauth2/revoke", - "revocation_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "revocation_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "introspection_endpoint": "http://mas:8080/oauth2/introspect", - "introspection_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "introspection_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "code_challenge_methods_supported": ["plain", "S256"], - "userinfo_endpoint": "http://mas:8080/oauth2/userinfo", - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": [ - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "ES256K", - ], - "userinfo_signing_alg_values_supported": [ - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "ES256K", - ], - "display_values_supported": ["page"], - "claim_types_supported": ["normal"], - "claims_supported": [ - "iss", - "sub", - "aud", - "iat", - "exp", - "nonce", - "auth_time", - "at_hash", - "c_hash", - ], - "claims_parameter_supported": false, - "request_parameter_supported": false, - "request_uri_parameter_supported": false, - "prompt_values_supported": ["none", "login", "create"], - "device_authorization_endpoint": "http://mas:8080/oauth2/device", - "org.matrix.matrix-authentication-service.graphql_endpoint": "http://mas:8080/graphql", - "account_management_uri": "http://mas:8080/account/", - "account_management_actions_supported": [ - "org.matrix.profile", - "org.matrix.sessions_list", - "org.matrix.session_view", - "org.matrix.session_end", - ], - }, - client_id: config.clients[0].client_id, - client_auth_method: config.clients[0].client_auth_method, - client_secret: config.clients[0].client_secret, - admin_token: config.matrix.secret, - account_management_url: `http://${container.getHost()}:${container.getMappedPort(8080)}/account`, - }, - }, - }); - - await use(container); - await container.stop(); + // eslint-disable-next-line no-empty-pattern + mas: async ({}, use) => { + // we stub the mas fixture to allow `homeserver` to depend on it to ensure + // when it is specified by `masHomeserver` it is started before the homeserver + await use(undefined); }, }); From 4fce0013c87452e847eb6dc99787a8343a023288 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 13:06:03 +0000 Subject: [PATCH 10/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups.spec.ts | 2 +- playwright/e2e/oidc/oidc-native.spec.ts | 8 +- .../homeserver/synapse/masHomeserver.ts | 166 +++--------------- playwright/services.ts | 3 +- playwright/testcontainers/mas.ts | 21 ++- 5 files changed, 53 insertions(+), 147 deletions(-) diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index baff4cd61ca..91c103702b5 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -31,7 +31,7 @@ test.describe("Encryption state after registration", () => { await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); await app.settings.openUserSettings("Security & Privacy"); - expect(page.getByText("This session is backing up your keys.")).toBeVisible(); + await expect(page.getByText("This session is backing up your keys.")).toBeVisible(); }); test("user is prompted to set up recovery", async ({ page, mailhogClient, app }) => { diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index f40c445fabf..60c5bbf025a 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -18,7 +18,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { test.slow(); // trace recording takes a while here test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => { - const tokenUri = `http://${mas.getHost()}:${mas.getMappedPort(8080)}/oauth2/token`; + const tokenUri = `${mas.baseUrl}/oauth2/token`; const tokenApiPromise = page.waitForRequest( (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", ); @@ -44,15 +44,15 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { // Assert MAS sees the session as OIDC Native const newPage = await newPagePromise; - await newPage.getByText("Sessions").click(); + await newPage.getByText("Devices").click(); await newPage.getByText(deviceId).click(); await expect(newPage.getByText("Element")).toBeVisible(); - await expect(newPage.getByText("oauth2_session:")).toBeVisible(); await expect(newPage.getByText("http://localhost:8080/")).toBeVisible(); + await expect(newPage).toHaveURL(/\/oauth2_session/); await newPage.close(); // Assert logging out revokes both tokens - const revokeUri = `http://${mas.getHost()}:${mas.getMappedPort(8080)}/oauth2/revoke`; + const revokeUri = `${mas.baseUrl}/oauth2/revoke`; const revokeAccessTokenPromise = page.waitForRequest( (request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token", ); diff --git a/playwright/plugins/homeserver/synapse/masHomeserver.ts b/playwright/plugins/homeserver/synapse/masHomeserver.ts index 446a0577f8d..cb144fcaeeb 100644 --- a/playwright/plugins/homeserver/synapse/masHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/masHomeserver.ts @@ -6,12 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { Fixtures } from "@playwright/test"; +import { Fixtures, PlaywrightTestArgs } from "@playwright/test"; import { Services } from "../../../services.ts"; +import { Fixtures as BaseFixtures } from "../../../element-web-test.ts"; import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts"; -export const masHomeserver: Fixtures = { +type Fixture = PlaywrightTestArgs & Services & BaseFixtures; +export const masHomeserver: Fixtures = { mas: async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => { const config = { clients: [ @@ -24,7 +26,7 @@ export const masHomeserver: Fixtures = { matrix: { homeserver: "localhost", secret: "AnotherRandomSecret", - endpoint: "http://synapse:8008", + endpoint: "http://homeserver:8008", }, }; @@ -43,144 +45,12 @@ export const masHomeserver: Fixtures = { experimental_features: { msc3861: { enabled: true, - issuer: "http://mas:8080/", - issuer_metadata: { - "issuer": `http://${container.getHost()}:${container.getMappedPort(8080)}/`, - "authorization_endpoint": "http://mas:8080/authorize", - "token_endpoint": "http://mas:8080/oauth2/token", - "jwks_uri": "http://mas:8080/oauth2/keys.json", - "registration_endpoint": "http://mas:8080/oauth2/registration", - "scopes_supported": ["openid", "email"], - "response_types_supported": ["code", "id_token", "code id_token"], - "response_modes_supported": ["form_post", "query", "fragment"], - "grant_types_supported": [ - "authorization_code", - "refresh_token", - "client_credentials", - "urn:ietf:params:oauth:grant-type:device_code", - ], - "token_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "token_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "revocation_endpoint": "http://mas:8080/oauth2/revoke", - "revocation_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "revocation_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "introspection_endpoint": "http://mas:8080/oauth2/introspect", - "introspection_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post", - "client_secret_jwt", - "private_key_jwt", - "none", - ], - "introspection_endpoint_auth_signing_alg_values_supported": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "code_challenge_methods_supported": ["plain", "S256"], - "userinfo_endpoint": "http://mas:8080/oauth2/userinfo", - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": [ - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "ES256K", - ], - "userinfo_signing_alg_values_supported": [ - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "ES256K", - ], - "display_values_supported": ["page"], - "claim_types_supported": ["normal"], - "claims_supported": [ - "iss", - "sub", - "aud", - "iat", - "exp", - "nonce", - "auth_time", - "at_hash", - "c_hash", - ], - "claims_parameter_supported": false, - "request_parameter_supported": false, - "request_uri_parameter_supported": false, - "prompt_values_supported": ["none", "login", "create"], - "device_authorization_endpoint": "http://mas:8080/oauth2/device", - "org.matrix.matrix-authentication-service.graphql_endpoint": "http://mas:8080/graphql", - "account_management_uri": "http://mas:8080/account/", - "account_management_actions_supported": [ - "org.matrix.profile", - "org.matrix.sessions_list", - "org.matrix.session_view", - "org.matrix.session_end", - ], - }, + issuer: `http://mas:8080/`, + introspection_endpoint: "http://mas:8080/oauth2/introspect", client_id: config.clients[0].client_id, client_auth_method: config.clients[0].client_auth_method, client_secret: config.clients[0].client_secret, admin_token: config.matrix.secret, - account_management_url: `http://${container.getHost()}:${container.getMappedPort(8080)}/account`, }, }, }); @@ -188,4 +58,26 @@ export const masHomeserver: Fixtures = { await use(container); await container.stop(); }, + + config: async ({ homeserver, context, mas }, use) => { + const issuer = `${mas.baseUrl}/`; + const wellKnown = { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + "org.matrix.msc2965.authentication": { + issuer, + account: `${issuer}account`, + }, + }; + + // Ensure org.matrix.msc2965.authentication is in well-known + await context.route("https://localhost/.well-known/matrix/client", async (route) => { + await route.fulfill({ json: wellKnown }); + }); + + await use({ + default_server_config: wellKnown, + }); + }, }; diff --git a/playwright/services.ts b/playwright/services.ts index 0e95e91fbe1..c05cb2d29dc 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -12,6 +12,7 @@ import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers import { StartedSynapseContainer, SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts"; import { ContainerLogger } from "./testcontainers/utils.ts"; +import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; export interface Services { logger: ContainerLogger; @@ -25,7 +26,7 @@ export interface Services { synapseConfigOptions: SynapseConfigOptions; _homeserver: SynapseContainer; homeserver: StartedSynapseContainer; - mas?: StartedTestContainer; + mas?: StartedMatrixAuthenticationServiceContainer; } export const test = base.extend({ diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index 79c354da4dc..2eadaafac66 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; import { StartedPostgreSqlContainer } from "@testcontainers/postgresql"; import * as YAML from "yaml"; @@ -133,6 +133,7 @@ const DEFAULT_CONFIG = { algorithm: "argon2id", }, ], + minimum_complexity: 0, }, policy: { wasm_module: "/usr/local/share/mas-cli/policy.wasm", @@ -158,6 +159,9 @@ const DEFAULT_CONFIG = { imprint: null, logo_uri: null, }, + account: { + password_registration_enabled: true, + }, experimental: { access_token_ttl: 300, compat_token_ttl: 300, @@ -168,7 +172,7 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer { private config: typeof DEFAULT_CONFIG; constructor(db: StartedPostgreSqlContainer) { - super("ghcr.io/matrix-org/matrix-authentication-service:0.8.0"); + super("ghcr.io/element-hq/matrix-authentication-service:0.12.0"); this.config = deepCopy(DEFAULT_CONFIG); this.config.database.username = db.getUsername(); @@ -187,7 +191,7 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer { return this; } - public override async start(): Promise { + public override async start(): Promise { const port = await getFreePort(); this.config.http.public_base = `http://localhost:${port}/`; @@ -203,6 +207,15 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer { }, ]); - return super.start(); + return new StartedMatrixAuthenticationServiceContainer(await super.start(), `http://localhost:${port}`); + } +} + +export class StartedMatrixAuthenticationServiceContainer extends AbstractStartedContainer { + constructor( + container: StartedTestContainer, + public readonly baseUrl: string, + ) { + super(container); } } From 4eefa275ab14f6eb0f7472e9e8d89683274776af Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 13:10:05 +0000 Subject: [PATCH 11/93] Update matrix-authentication-service in Playwright tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups.spec.ts | 2 +- playwright/e2e/oidc/oidc-native.spec.ts | 4 +- .../templates/mas-oidc/homeserver.yaml | 100 +----------------- .../matrix-authentication-service/config.yaml | 3 + 4 files changed, 7 insertions(+), 102 deletions(-) diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index 5936c2ede5f..9d98534a47c 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -29,7 +29,7 @@ masTest.describe("Encryption state after registration", () => { await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); await app.settings.openUserSettings("Security & Privacy"); - expect(page.getByText("This session is backing up your keys.")).toBeVisible(); + await expect(page.getByText("This session is backing up your keys.")).toBeVisible(); }); masTest("user is prompted to set up recovery", async ({ page, mailhog, app }) => { diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index f8dd24daa65..e2e7a816dd2 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -41,11 +41,11 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { // Assert MAS sees the session as OIDC Native const newPage = await newPagePromise; - await newPage.getByText("Sessions").click(); + await newPage.getByText("Devices").click(); await newPage.getByText(deviceId).click(); await expect(newPage.getByText("Element")).toBeVisible(); - await expect(newPage.getByText("oauth2_session:")).toBeVisible(); await expect(newPage.getByText("http://localhost:8080/")).toBeVisible(); + await expect(newPage).toHaveURL(/\/oauth2_session/); await newPage.close(); // Assert logging out revokes both tokens diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml index 802d97acade..147944b89f8 100644 --- a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml +++ b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml @@ -83,102 +83,7 @@ experimental_features: enabled: true issuer: http://localhost:%MAS_PORT%/ - # We have to bake in the metadata here as we need to override `introspection_endpoint` - issuer_metadata: { - "issuer": "http://localhost:%MAS_PORT%/", - "authorization_endpoint": "http://localhost:%MAS_PORT%/authorize", - "token_endpoint": "http://localhost:%MAS_PORT%/oauth2/token", - "jwks_uri": "http://localhost:%MAS_PORT%/oauth2/keys.json", - "registration_endpoint": "http://localhost:%MAS_PORT%/oauth2/registration", - "scopes_supported": ["openid", "email"], - "response_types_supported": ["code", "id_token", "code id_token"], - "response_modes_supported": ["form_post", "query", "fragment"], - "grant_types_supported": - [ - "authorization_code", - "refresh_token", - "client_credentials", - "urn:ietf:params:oauth:grant-type:device_code", - ], - "token_endpoint_auth_methods_supported": - ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], - "token_endpoint_auth_signing_alg_values_supported": - [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "revocation_endpoint": "http://localhost:%MAS_PORT%/oauth2/revoke", - "revocation_endpoint_auth_methods_supported": - ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], - "revocation_endpoint_auth_signing_alg_values_supported": - [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - # This is the only changed value - "introspection_endpoint": "http://host.containers.internal:%MAS_PORT%/oauth2/introspect", - "introspection_endpoint_auth_methods_supported": - ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], - "introspection_endpoint_auth_signing_alg_values_supported": - [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "PS256", - "PS384", - "PS512", - "ES256", - "ES384", - "ES256K", - ], - "code_challenge_methods_supported": ["plain", "S256"], - "userinfo_endpoint": "http://localhost:%MAS_PORT%/oauth2/userinfo", - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": - ["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"], - "userinfo_signing_alg_values_supported": - ["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"], - "display_values_supported": ["page"], - "claim_types_supported": ["normal"], - "claims_supported": ["iss", "sub", "aud", "iat", "exp", "nonce", "auth_time", "at_hash", "c_hash"], - "claims_parameter_supported": false, - "request_parameter_supported": false, - "request_uri_parameter_supported": false, - "prompt_values_supported": ["none", "login", "create"], - "device_authorization_endpoint": "http://localhost:%MAS_PORT%/oauth2/device", - "org.matrix.matrix-authentication-service.graphql_endpoint": "http://localhost:%MAS_PORT%/graphql", - "account_management_uri": "http://localhost:%MAS_PORT%/account/", - "account_management_actions_supported": - [ - "org.matrix.profile", - "org.matrix.sessions_list", - "org.matrix.session_view", - "org.matrix.session_end", - ], - } + introspection_endpoint: "http://localhost:%MAS_PORT%/oauth2/introspect", # Matches the `client_id` in the auth service config client_id: 0000000000000000000SYNAPSE @@ -189,6 +94,3 @@ experimental_features: # Matches the `matrix.secret` in the auth service config admin_token: "AnotherRandomSecret" - - # URL to advertise to clients where users can self-manage their account - account_management_url: "http://localhost:%MAS_PORT%/account" diff --git a/playwright/plugins/matrix-authentication-service/config.yaml b/playwright/plugins/matrix-authentication-service/config.yaml index e7ab83e736e..5ee69bdec5a 100644 --- a/playwright/plugins/matrix-authentication-service/config.yaml +++ b/playwright/plugins/matrix-authentication-service/config.yaml @@ -125,6 +125,7 @@ passwords: schemes: - version: 1 algorithm: argon2id + minimum_complexity: 0 matrix: homeserver: localhost secret: AnotherRandomSecret @@ -148,6 +149,8 @@ branding: tos_uri: null imprint: null logo_uri: null +account: + password_registration_enabled: true experimental: access_token_ttl: 300 compat_token_ttl: 300 From 1cd1dcd65240827d8fe53b4eeeb70e4254346e77 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 13:12:59 +0000 Subject: [PATCH 12/93] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../homeserver/synapse/templates/mas-oidc/homeserver.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml index 147944b89f8..c2badec759b 100644 --- a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml +++ b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml @@ -83,7 +83,7 @@ experimental_features: enabled: true issuer: http://localhost:%MAS_PORT%/ - introspection_endpoint: "http://localhost:%MAS_PORT%/oauth2/introspect", + introspection_endpoint: "http://localhost:%MAS_PORT%/oauth2/introspect" # Matches the `client_id` in the auth service config client_id: 0000000000000000000SYNAPSE From f1c392dd085c11fca801463026d70e1e1de091c2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 13:22:44 +0000 Subject: [PATCH 13/93] Fix SMTP port Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/plugins/homeserver/synapse/emailHomeserver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/plugins/homeserver/synapse/emailHomeserver.ts b/playwright/plugins/homeserver/synapse/emailHomeserver.ts index a8b4cbf0264..ab7affdee52 100644 --- a/playwright/plugins/homeserver/synapse/emailHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/emailHomeserver.ts @@ -18,7 +18,7 @@ export const emailHomeserver: Fixtures = { registrations_require_3pid: ["email"], email: { smtp_host: "mailhog", - smtp_port: 25, + smtp_port: 1025, notif_from: "Your Friendly %(app)s homeserver ", app_name: "my_branded_matrix_server", }, From 84126e8ed0b864c9f06ee38237511bfed99974c4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 13:31:15 +0000 Subject: [PATCH 14/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../homeserver/synapse/templates/mas-oidc/homeserver.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml index c2badec759b..64fea9a5a97 100644 --- a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml +++ b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml @@ -82,8 +82,8 @@ experimental_features: msc3861: enabled: true - issuer: http://localhost:%MAS_PORT%/ - introspection_endpoint: "http://localhost:%MAS_PORT%/oauth2/introspect" + issuer: http://host.containers.internal:%MAS_PORT%/ + introspection_endpoint: http://host.containers.internal:%MAS_PORT%/oauth2/introspect # Matches the `client_id` in the auth service config client_id: 0000000000000000000SYNAPSE From eaca3f83d6b3f0d06bfb1349ebdfd98b86003170 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 14:45:15 +0000 Subject: [PATCH 15/93] Comments Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- docs/playwright.md | 6 +++++- playwright/testcontainers/mas.ts | 2 ++ playwright/testcontainers/synapse.ts | 14 ++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/playwright.md b/docs/playwright.md index 73ee77228b7..fe44a06ff17 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -227,7 +227,11 @@ has to be disabled in Playwright on Firefox & Webkit to retain routing functiona Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available there at this time. -## Colima +## Supporter container runtimes + +We use testcontainers to spin up various instances of Synapse, Matrix Authentication Service, and more. +It supports Docker out of the box but also has support for Podman, Colima, Rancher, you just need to follow some instructions to achieve it: +https://node.testcontainers.org/supported-container-runtimes/ If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path within `$HOME` to allow bind mounting temporary directories into the Docker containers. diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index 2eadaafac66..0c5dae89d0c 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -144,7 +144,9 @@ const DEFAULT_CONFIG = { email_entrypoint: "email/violation", data: { client_registration: { + // allow non-SSL and localhost URIs allow_insecure_uris: true, + // EW doesn't have contacts at this time allow_missing_contacts: true, }, }, diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 96104d6272d..4cc5c3e7d8e 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -20,13 +20,15 @@ const TAG = "develop@sha256:17cc0a301447430624afb860276e5c13270ddeb99a3f6d1c6d51 const DEFAULT_CONFIG = { server_name: "localhost", - public_baseurl: "", + public_baseurl: "", // set by start method pid_file: "/homeserver.pid", web_client: false, soft_file_limit: 0, + // Needs to be configured to log to the console like a good docker process log_config: "/data/log.config", listeners: [ { + // Listener is always port 8008 (configured in the container) port: 8008, tls: false, bind_addresses: ["::"], @@ -41,6 +43,7 @@ const DEFAULT_CONFIG = { }, ], database: { + // An sqlite in-memory database is fast & automatically wipes each time name: "sqlite3", args: { database: ":memory:", @@ -102,11 +105,13 @@ const DEFAULT_CONFIG = { enable_registration_without_verification: true, disable_msisdn_registration: false, registrations_require_3pid: [], - registration_shared_secret: "secret", enable_metrics: false, report_stats: false, + // These placeholders will be replaced with values generated at start + registration_shared_secret: "secret", macaroon_secret_key: "secret", form_secret: "secret", + // Signing key must be here: it will be generated to this file signing_key_path: "/data/localhost.signing.key", trusted_key_servers: [], password_config: { @@ -116,6 +121,7 @@ const DEFAULT_CONFIG = { session_timeout: "300s", }, background_updates: { + // Inhibit background updates as this Synapse isn't long-lived min_batch_size: 100000, sleep_duration_ms: 100000, }, @@ -144,9 +150,9 @@ export class SynapseContainer extends GenericContainer { const signingKey = randB64Bytes(32); this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([ - { target: "/data/localhost.signing.key", content: `ed25519 x ${signingKey}` }, + { target: this.config.signing_key_path, content: `ed25519 x ${signingKey}` }, { - target: "/data/log.config", + target: this.config.log_config, content: YAML.stringify({ version: 1, formatters: { From edefe021747b9cf8c6717d238955d2e24824e998 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 14:53:20 +0000 Subject: [PATCH 16/93] Strip ansi from playwright logs to make them more readable Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 + playwright/testcontainers/utils.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e00cb47f4a..c77d6ce7880 100644 --- a/package.json +++ b/package.json @@ -275,6 +275,7 @@ "rimraf": "^6.0.0", "semver": "^7.5.2", "source-map-loader": "^5.0.0", + "strip-ansi": "^7.1.0", "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", diff --git a/playwright/testcontainers/utils.ts b/playwright/testcontainers/utils.ts index d4fe32b4d82..36b8d8aabf7 100644 --- a/playwright/testcontainers/utils.ts +++ b/playwright/testcontainers/utils.ts @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. import { TestInfo } from "@playwright/test"; import { Readable } from "stream"; +import stripAnsi from "strip-ansi"; export class ContainerLogger { private logs: Record = {}; @@ -27,7 +28,7 @@ export class ContainerLogger { if (testInfo.status !== "passed") { for (const container in this.logs) { await testInfo.attach(container, { - body: this.logs[container], + body: stripAnsi(this.logs[container]), contentType: "text/plain", }); } From d506dec51a3f950f4262f4415bc27e7808eccd45 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 15:09:39 +0000 Subject: [PATCH 17/93] Actually do the update Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../plugins/matrix-authentication-service/index.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/playwright/plugins/matrix-authentication-service/index.ts b/playwright/plugins/matrix-authentication-service/index.ts index eeccd4f4950..d752b92e524 100644 --- a/playwright/plugins/matrix-authentication-service/index.ts +++ b/playwright/plugins/matrix-authentication-service/index.ts @@ -18,8 +18,7 @@ import { HomeserverInstance } from "../homeserver"; import { Instance as MailhogInstance } from "../mailhog"; // Docker tag to use for `ghcr.io/matrix-org/matrix-authentication-service` image. -// We use a debug tag so that we have a shell and can run all 3 necessary commands in one run. -const TAG = "0.8.0-debug"; +const TAG = "0.12.0"; export interface ProxyInstance { containerId: string; @@ -87,15 +86,10 @@ export class MatrixAuthenticationService { console.log(new Date(), "starting mas container...", TAG); const containerId = await this.masDocker.run({ - image: "ghcr.io/matrix-org/matrix-authentication-service:" + TAG, + image: "ghcr.io/element-hq/matrix-authentication-service:" + TAG, containerName: "react-sdk-playwright-mas", - params: ["-p", `${port}:8080/tcp`, "-v", `${configDir}:/config`, "--entrypoint", "sh"], - cmd: [ - "-c", - "mas-cli database migrate --config /config/config.yaml && " + - "mas-cli config sync --config /config/config.yaml && " + - "mas-cli server --config /config/config.yaml", - ], + params: ["-p", `${port}:8080/tcp`, "-v", `${configDir}:/config`], + cmd: ["mas-cli", "server", "--config", "/config/config.yaml"], }); console.log(new Date(), "started!"); From 63e855e0c4f8f092848377e77c4a207f640f38a6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 15:25:40 +0000 Subject: [PATCH 18/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/dehydration.spec.ts | 49 +++++++++++------------ playwright/e2e/crypto/migration.spec.ts | 32 +++++++-------- playwright/testcontainers/dendrite.ts | 14 +++---- playwright/testcontainers/mas.ts | 1 + playwright/testcontainers/synapse.ts | 1 + 5 files changed, 46 insertions(+), 51 deletions(-) diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 64d0c4888cd..fe0aad51b98 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -8,35 +8,10 @@ Please see LICENSE files in the repository root for full details. import { Locator, type Page } from "@playwright/test"; -import { test as base, expect, Fixtures } from "../../element-web-test"; +import { test, expect } from "../../element-web-test"; import { viewRoomSummaryByName } from "../right-panel/utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; -const test = base.extend({ - synapseConfigOptions: { - experimental_features: { - msc2697_enabled: false, - msc3814_enabled: true, - }, - }, - config: async ({ homeserver, context }, use) => { - const wellKnown = { - "m.homeserver": { - base_url: homeserver.baseUrl, - }, - "org.matrix.msc3814": true, - }; - - await context.route("https://localhost/.well-known/matrix/client", async (route) => { - await route.fulfill({ json: wellKnown }); - }); - - await use({ - default_server_config: wellKnown, - }); - }, -}); - const ROOM_NAME = "Test room"; const NAME = "Alice"; @@ -49,6 +24,28 @@ test.describe("Dehydration", () => { test.use({ displayName: NAME, + synapseConfigOptions: { + experimental_features: { + msc2697_enabled: false, + msc3814_enabled: true, + }, + }, + config: async ({ homeserver, context }, use) => { + const wellKnown = { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + "org.matrix.msc3814": true, + }; + + await context.route("https://localhost/.well-known/matrix/client", async (route) => { + await route.fulfill({ json: wellKnown }); + }); + + await use({ + default_server_config: wellKnown, + }); + }, }); test("Create dehydrated device", async ({ page, user, app }, workerInfo) => { diff --git a/playwright/e2e/crypto/migration.spec.ts b/playwright/e2e/crypto/migration.spec.ts index c36cf2997ee..86072fccc07 100644 --- a/playwright/e2e/crypto/migration.spec.ts +++ b/playwright/e2e/crypto/migration.spec.ts @@ -9,24 +9,24 @@ Please see LICENSE files in the repository root for full details. import path from "path"; import { readFile } from "node:fs/promises"; -import { expect, Fixtures, test as base } from "../../element-web-test"; - -const test = base.extend({ - // Replace the `user` fixture with one which populates the indexeddb data before starting the app. - user: async ({ context, pageWithCredentials: page, credentials }, use) => { - await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => { - const resourcePath = path.join(__dirname, new URL(request.url()).pathname); - const body = await readFile(resourcePath, { encoding: "utf-8" }); - await route.fulfill({ body }); - }); - await page.goto("/test_indexeddb_cryptostore_dump/index.html"); - - await use(credentials); - }, -}); +import { expect, test } from "../../element-web-test"; test.describe("migration", { tag: "@no-webkit" }, function () { - test.use({ displayName: "Alice" }); + test.use({ + displayName: "Alice", + + // Replace the `user` fixture with one which populates the indexeddb data before starting the app. + user: async ({ context, pageWithCredentials: page, credentials }, use) => { + await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => { + const resourcePath = path.join(__dirname, new URL(request.url()).pathname); + const body = await readFile(resourcePath, { encoding: "utf-8" }); + await route.fulfill({ body }); + }); + await page.goto("/test_indexeddb_cryptostore_dump/index.html"); + + await use(credentials); + }, + }); test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => { test.slow(); diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index f066791e2b4..141d96156b6 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -10,7 +10,6 @@ import { APIRequestContext } from "@playwright/test"; import * as YAML from "yaml"; import { set } from "lodash"; -import { getFreePort } from "../plugins/utils/port.ts"; import { randB64Bytes } from "../plugins/utils/rand.ts"; import { StartedSynapseContainer } from "./synapse.ts"; import { deepCopy } from "../plugins/utils/object.ts"; @@ -223,6 +222,7 @@ export class DendriteContainer extends GenericContainer { "-c", `/usr/bin/generate-keys -private-key /etc/dendrite/matrix_key.pem && ${binary} --config /etc/dendrite/dendrite.yaml --really-enable-open-registration true run`, ]) + .withExposedPorts(8008) .withWaitStrategy(Wait.forHttp("/_matrix/client/versions", 8008)); } @@ -240,22 +240,18 @@ export class DendriteContainer extends GenericContainer { } public override async start(): Promise { - const port = await getFreePort(); - - this.withExposedPorts({ - container: 8008, - host: port, - }).withCopyContentToContainer([ + this.withCopyContentToContainer([ { target: "/etc/dendrite/dendrite.yaml", content: YAML.stringify(this.config), }, ]); + const container = await super.start(); // Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it return new StartedSynapseContainer( - await super.start(), - `http://localhost:${port}`, + container, + `http://${container.getHost()}:${container.getMappedPort(8008)}`, this.config.client_api.registration_shared_secret, this.request, ); diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index 0c5dae89d0c..d15f619dbc6 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -194,6 +194,7 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer { } public override async start(): Promise { + // MAS config issuer needs to know what URL it'll be accessed from, so we have to map the port manually const port = await getFreePort(); this.config.http.public_base = `http://localhost:${port}/`; diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 4cc5c3e7d8e..a04041437f3 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -199,6 +199,7 @@ export class SynapseContainer extends GenericContainer { } public override async start(): Promise { + // Synapse config public_baseurl needs to know what URL it'll be accessed from, so we have to map the port manually const port = await getFreePort(); this.withExposedPorts({ From 8e1372b0b468742521f472692aeefaaa6f1ccb57 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 15:29:52 +0000 Subject: [PATCH 19/93] Remove access to homeserver.config.baseUrl field in favour of homeserver.baseUrl Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/app-loading/guest-registration.spec.ts | 2 +- playwright/e2e/crypto/dehydration.spec.ts | 2 +- playwright/e2e/crypto/utils.ts | 2 +- .../forgot-password/forgot-password.spec.ts | 4 ++-- playwright/e2e/login/login.spec.ts | 18 +++++++++--------- playwright/e2e/login/overwrite_login.spec.ts | 2 +- playwright/e2e/login/utils.ts | 2 +- playwright/e2e/oidc/index.ts | 2 +- playwright/e2e/register/email.spec.ts | 2 +- playwright/e2e/register/register.spec.ts | 4 ++-- playwright/element-web-test.ts | 2 +- playwright/pages/bot.ts | 6 +++--- playwright/pages/crypto.ts | 2 +- playwright/plugins/homeserver/index.ts | 1 + playwright/plugins/homeserver/synapse/index.ts | 4 ++++ 15 files changed, 30 insertions(+), 25 deletions(-) diff --git a/playwright/e2e/app-loading/guest-registration.spec.ts b/playwright/e2e/app-loading/guest-registration.spec.ts index 78ba57ad6bf..fe4215452b0 100644 --- a/playwright/e2e/app-loading/guest-registration.spec.ts +++ b/playwright/e2e/app-loading/guest-registration.spec.ts @@ -17,7 +17,7 @@ test.use({ config: async ({ homeserver }, use) => { await use({ default_server_config: { - "m.homeserver": { base_url: homeserver.config.baseUrl }, + "m.homeserver": { base_url: homeserver.baseUrl }, }, }); }, diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index b2aa65b1a25..4f87433f295 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -20,7 +20,7 @@ const test = base.extend({ config: async ({ homeserver, context }, use) => { const wellKnown = { "m.homeserver": { - base_url: homeserver.config.baseUrl, + base_url: homeserver.baseUrl, }, "org.matrix.msc3814": true, }; diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index b170b24d669..f7f00ef387a 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -148,7 +148,7 @@ export async function logIntoElement( // select homeserver await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index 9ffacc8efc9..e53cb52dd45 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -32,7 +32,7 @@ test.describe("Forgot Password", () => { await page.getByRole("link", { name: "Sign in" }).click(); // need to select a homeserver at this stage, before entering the forgot password flow - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("button", { name: "Forgot password?" }).click(); @@ -47,7 +47,7 @@ test.describe("Forgot Password", () => { await page.goto("/"); await page.getByRole("link", { name: "Sign in" }).click(); - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("button", { name: "Forgot password?" }).click(); diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login.spec.ts index 864f41922a9..40c2860f957 100644 --- a/playwright/e2e/login/login.spec.ts +++ b/playwright/e2e/login/login.spec.ts @@ -70,7 +70,7 @@ const DEVICE_SIGNING_KEYS_BODY = { async function login(page: Page, homeserver: HomeserverInstance) { await page.getByRole("link", { name: "Sign in" }).click(); - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("textbox", { name: "Username" }).fill(username); await page.getByPlaceholder("Password").fill(password); @@ -101,7 +101,7 @@ test.describe("Login", () => { await page.getByRole("link", { name: "Sign in" }).click(); // first pick the homeserver, as otherwise the user picker won't be visible - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await page.getByRole("button", { name: "Edit" }).click(); @@ -114,7 +114,7 @@ test.describe("Login", () => { await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid"); // switch back to the custom homeserver - await selectHomeserver(page, homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.baseUrl); await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688 @@ -142,10 +142,10 @@ test.describe("Login", () => { homeserver, request, }) => { - const res = await request.post( - `${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, - { headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY }, - ); + const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { + headers: { Authorization: `Bearer ${creds.accessToken}` }, + data: DEVICE_SIGNING_KEYS_BODY, + }); if (res.status() / 100 !== 2) { console.log("Uploading dummy keys failed", await res.json()); } @@ -172,7 +172,7 @@ test.describe("Login", () => { request, }) => { const res = await request.post( - `${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, + `${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY }, ); if (res.status() / 100 !== 2) { @@ -203,7 +203,7 @@ test.describe("Login", () => { }) => { console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY); const res = await request.post( - `${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, + `${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY }, ); if (res.status() / 100 !== 2) { diff --git a/playwright/e2e/login/overwrite_login.spec.ts b/playwright/e2e/login/overwrite_login.spec.ts index 7d5ec6a649d..7fa1ccc7ece 100644 --- a/playwright/e2e/login/overwrite_login.spec.ts +++ b/playwright/e2e/login/overwrite_login.spec.ts @@ -24,7 +24,7 @@ test.describe("Overwrite login action", () => { expect(credentials.userId).not.toBe(bobRegister.userId); const clientCredentials /* IMatrixClientCreds */ = { - homeserverUrl: homeserver.config.baseUrl, + homeserverUrl: homeserver.baseUrl, ...bobRegister, }; diff --git a/playwright/e2e/login/utils.ts b/playwright/e2e/login/utils.ts index 2c576dbea74..0a728faecc1 100644 --- a/playwright/e2e/login/utils.ts +++ b/playwright/e2e/login/utils.ts @@ -19,7 +19,7 @@ export async function doTokenRegistration( await page.goto("/#/login"); await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue" }).click(); // wait for the dialog to go away await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 7a99677968b..2f962e6b6d0 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -44,7 +44,7 @@ export const test = base.extend<{ const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`; const wellKnown = { "m.homeserver": { - base_url: homeserver.config.baseUrl, + base_url: homeserver.baseUrl, }, "org.matrix.msc2965.authentication": { issuer, diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 12d706ab3c8..20fa6802298 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -25,7 +25,7 @@ test.describe("Email Registration", async () => { use({ default_server_config: { "m.homeserver": { - base_url: homeserver.config.baseUrl, + base_url: homeserver.baseUrl, }, "m.identity_server": { base_url: "https://server.invalid", diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index 553972d6cfb..c1b70de6947 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -27,7 +27,7 @@ test.describe("Registration", () => { await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); await checkA11y(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away await expect(page.getByRole("dialog")).not.toBeVisible(); @@ -88,7 +88,7 @@ test.describe("Registration", () => { test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => { await page.getByRole("button", { name: "Edit", exact: true }).click(); await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away await expect(page.getByRole("dialog")).not.toBeVisible(); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 6803da9e16b..4f85fd61ee1 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -220,7 +220,7 @@ export const test = base.extend({ // Ensure the language is set to a consistent value window.localStorage.setItem("mx_local_settings", '{"language":"en"}'); }, - { baseUrl: homeserver.config.baseUrl, credentials }, + { baseUrl: homeserver.baseUrl, credentials }, ); await use(page); }, diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index fbb7fd90101..1d414c7bf6a 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -97,7 +97,7 @@ export class Bot extends Client { private async buildClient(): Promise> { const credentials = await this.getCredentials(); const clientHandle = await this.page.evaluateHandle( - async ({ homeserver, credentials, opts }) => { + async ({ baseUrl, credentials, opts }) => { function getLogger(loggerName: string): Logger { const logger = { getChild: (namespace: string) => getLogger(`${loggerName}:${namespace}`), @@ -157,7 +157,7 @@ export class Bot extends Client { }; const cli = new window.matrixcs.MatrixClient({ - baseUrl: homeserver.baseUrl, + baseUrl, userId: credentials.userId, deviceId: credentials.deviceId, accessToken: credentials.accessToken, @@ -179,7 +179,7 @@ export class Bot extends Client { return cli; }, { - homeserver: this.homeserver.config, + baseUrl: this.homeserver.baseUrl, credentials, opts: this.opts, }, diff --git a/playwright/pages/crypto.ts b/playwright/pages/crypto.ts index 934c81d7f6a..c31e7fbedb3 100644 --- a/playwright/pages/crypto.ts +++ b/playwright/pages/crypto.ts @@ -27,7 +27,7 @@ export class Crypto { accessToken: window.mxMatrixClientPeg.get().getAccessToken(), })); - const res = await this.request.post(`${this.homeserver.config.baseUrl}/_matrix/client/v3/keys/query`, { + const res = await this.request.post(`${this.homeserver.baseUrl}/_matrix/client/v3/keys/query`, { headers: { Authorization: `Bearer ${accessToken}` }, data: { device_keys: { [userId]: [] } }, }); diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index c6a09ceab7f..c0f07a7bcde 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -16,6 +16,7 @@ export interface HomeserverConfig { export interface HomeserverInstance { readonly config: HomeserverConfig; + readonly baseUrl: string; /** * Register a user on the given Homeserver using the shared registration secret. diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 574797ae750..ea9d8e3c2e3 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -146,6 +146,10 @@ export class Synapse implements Homeserver, HomeserverInstance { return [path.join(synapseLogsPath, "stdout.log"), path.join(synapseLogsPath, "stderr.log")]; } + public get baseUrl(): string { + return this.config.baseUrl; + } + private async registerUserInternal( username: string, password: string, From d80ad9d358aff27bb0224cb210888560296ae62c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 15:40:19 +0000 Subject: [PATCH 20/93] Use sane default_server_config and specify server.invalid in the specific tests which demand it Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../app-loading/guest-registration.spec.ts | 7 ----- .../e2e/crypto/complete-security.spec.ts | 11 +++++++- .../decryption-failure-messages.spec.ts | 4 +-- playwright/e2e/crypto/dehydration.spec.ts | 10 +++---- .../e2e/crypto/device-verification.spec.ts | 10 +++---- playwright/e2e/crypto/event-shields.spec.ts | 2 +- playwright/e2e/crypto/logout.spec.ts | 2 +- playwright/e2e/crypto/utils.ts | 15 +---------- .../forgot-password/forgot-password.spec.ts | 9 +++++++ playwright/e2e/login/login.spec.ts | 12 +++++++++ playwright/e2e/login/overwrite_login.spec.ts | 2 +- playwright/e2e/login/soft_logout.spec.ts | 9 +++++++ playwright/e2e/oidc/index.ts | 10 +++---- playwright/e2e/register/email.spec.ts | 7 +++-- playwright/element-web-test.ts | 26 +++++++++---------- 15 files changed, 72 insertions(+), 64 deletions(-) diff --git a/playwright/e2e/app-loading/guest-registration.spec.ts b/playwright/e2e/app-loading/guest-registration.spec.ts index fe4215452b0..ea732e59ece 100644 --- a/playwright/e2e/app-loading/guest-registration.spec.ts +++ b/playwright/e2e/app-loading/guest-registration.spec.ts @@ -14,13 +14,6 @@ import { expect, test } from "../../element-web-test"; test.use({ startHomeserverOpts: "guest-enabled", - config: async ({ homeserver }, use) => { - await use({ - default_server_config: { - "m.homeserver": { base_url: homeserver.baseUrl }, - }, - }); - }, }); test("Shows the welcome page by default", async ({ page }) => { diff --git a/playwright/e2e/crypto/complete-security.spec.ts b/playwright/e2e/crypto/complete-security.spec.ts index 44eb70355c8..9d85f85356e 100644 --- a/playwright/e2e/crypto/complete-security.spec.ts +++ b/playwright/e2e/crypto/complete-security.spec.ts @@ -12,6 +12,15 @@ import { logIntoElement } from "./utils"; test.describe("Complete security", () => { test.use({ displayName: "Jeff", + config: { + // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. + // We point that to a guaranteed-invalid domain. + default_server_config: { + "m.homeserver": { + base_url: "https://server.invalid", + }, + }, + }, }); test("should go straight to the welcome screen if we have no signed device", async ({ @@ -19,7 +28,7 @@ test.describe("Complete security", () => { homeserver, credentials, }) => { - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); await expect(page.getByText("Welcome Jeff", { exact: true })).toBeVisible(); }); diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts index b9199ef9fd8..e1952bfec60 100644 --- a/playwright/e2e/crypto/decryption-failure-messages.spec.ts +++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -45,7 +45,7 @@ test.describe("Cryptography", function () { await logOutOfElement(page, true); // Log in again, and see how the message looks. - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); await app.viewRoomByName("Test room"); const lastTile = page.locator(".mx_EventTile").last(); await expect(lastTile).toContainText("Historical messages are not available on this device"); @@ -62,7 +62,7 @@ test.describe("Cryptography", function () { // Finally, log out again, and back in, skipping verification for now, and see what we see. await logOutOfElement(page); - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await app.viewRoomByName("Test room"); diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 4f87433f295..aa4df542a56 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -17,11 +17,9 @@ const test = base.extend({ startHomeserverOpts: async ({}, use) => { await use("dehydration"); }, - config: async ({ homeserver, context }, use) => { + context: async ({ config, context }, use) => { const wellKnown = { - "m.homeserver": { - base_url: homeserver.baseUrl, - }, + ...config.default_server_config, "org.matrix.msc3814": true, }; @@ -29,9 +27,7 @@ const test = base.extend({ await route.fulfill({ json: wellKnown }); }); - await use({ - default_server_config: wellKnown, - }); + await use(context); }, }); diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index ddd564139f0..a028bfb70c2 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -66,7 +66,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { } test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => { - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); // Launch the verification request between alice and the bot const verificationRequest = await initiateAliceVerificationRequest(page); @@ -93,7 +93,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => { // A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key" - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); // Launch the verification request between alice and the bot const verificationRequest = await initiateAliceVerificationRequest(page); @@ -137,7 +137,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { }); test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => { - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); // Select the security phrase await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click(); @@ -158,7 +158,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { }); test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => { - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); // Select the security phrase await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click(); @@ -181,7 +181,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { }); test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => { - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); /* Dismiss "Verify this device" */ const authPage = page.locator(".mx_AuthPage"); diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index f680f340c3d..3811c2819e5 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -207,7 +207,7 @@ test.describe("Cryptography", function () { window.localStorage.clear(); }); await page.reload(); - await logIntoElement(page, homeserver, aliceCredentials, securityKey); + await logIntoElement(page, aliceCredentials, securityKey); /* go back to the test room and find Bob's message again */ await app.viewRoomById(testRoomId); diff --git a/playwright/e2e/crypto/logout.spec.ts b/playwright/e2e/crypto/logout.spec.ts index 8e408dc6344..2bafe0ece88 100644 --- a/playwright/e2e/crypto/logout.spec.ts +++ b/playwright/e2e/crypto/logout.spec.ts @@ -11,7 +11,7 @@ import { createRoom, enableKeyBackup, logIntoElement, sendMessageInCurrentRoom } test.describe("Logout tests", () => { test.beforeEach(async ({ page, homeserver, credentials }) => { - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); }); test("Ask to set up recovery on logout if not setup", async ({ page, app }) => { diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index f7f00ef387a..48da798f1a7 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -138,22 +138,9 @@ export async function checkDeviceIsConnectedKeyBackup( * * If a `securityKey` is given, verifies the new device using the key. */ -export async function logIntoElement( - page: Page, - homeserver: HomeserverInstance, - credentials: Credentials, - securityKey?: string, -) { +export async function logIntoElement(page: Page, credentials: Credentials, securityKey?: string) { await page.goto("/#/login"); - // select homeserver - await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); - await page.getByRole("button", { name: "Continue", exact: true }).click(); - - // wait for the dialog to go away - await expect(page.locator(".mx_ServerPickerDialog")).not.toBeVisible(); - await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId); await page.getByPlaceholder("Password").fill(credentials.password); await page.getByRole("button", { name: "Sign in" }).click(); diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index e53cb52dd45..8033f0a1236 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -16,6 +16,15 @@ const email = "user@nowhere.dummy"; test.describe("Forgot Password", () => { test.use({ + config: { + // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. + // We point that to a guaranteed-invalid domain. + default_server_config: { + "m.homeserver": { + base_url: "https://server.invalid", + }, + }, + }, startHomeserverOpts: ({ mailhog }, use) => use({ template: "email", diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login.spec.ts index 40c2860f957..7bab38e015d 100644 --- a/playwright/e2e/login/login.spec.ts +++ b/playwright/e2e/login/login.spec.ts @@ -78,6 +78,18 @@ async function login(page: Page, homeserver: HomeserverInstance) { } test.describe("Login", () => { + test.use({ + config: { + // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. + // We point that to a guaranteed-invalid domain. + default_server_config: { + "m.homeserver": { + base_url: "https://server.invalid", + }, + }, + }, + }); + test.describe("Password login", () => { test.use({ startHomeserverOpts: "consent" }); diff --git a/playwright/e2e/login/overwrite_login.spec.ts b/playwright/e2e/login/overwrite_login.spec.ts index 7fa1ccc7ece..4beed00d120 100644 --- a/playwright/e2e/login/overwrite_login.spec.ts +++ b/playwright/e2e/login/overwrite_login.spec.ts @@ -13,7 +13,7 @@ test.describe("Overwrite login action", () => { // This seems terminally flakey: https://github.com/element-hq/element-web/issues/27363 // I tried verious things to try & deflake it, to no avail: https://github.com/matrix-org/matrix-react-sdk/pull/12506 test.skip("Try replace existing login with new one", async ({ page, app, credentials, homeserver }) => { - await logIntoElement(page, homeserver, credentials); + await logIntoElement(page, credentials); const userMenu = await app.openUserMenu(); await expect(userMenu.getByText(credentials.userId)).toBeVisible(); diff --git a/playwright/e2e/login/soft_logout.spec.ts b/playwright/e2e/login/soft_logout.spec.ts index cb8f832f9dd..dec6b409c05 100644 --- a/playwright/e2e/login/soft_logout.spec.ts +++ b/playwright/e2e/login/soft_logout.spec.ts @@ -16,6 +16,15 @@ import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Soft logout", () => { test.use({ displayName: "Alice", + config: { + // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. + // We point that to a guaranteed-invalid domain. + default_server_config: { + "m.homeserver": { + base_url: "https://server.invalid", + }, + }, + }, startHomeserverOpts: ({ oAuthServer }, use) => use({ template: "default", diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 2f962e6b6d0..cb6b71b455c 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -40,12 +40,10 @@ export const test = base.extend<{ }, }); }, - config: async ({ homeserver, startHomeserverOpts, context }, use) => { + context: async ({ config, startHomeserverOpts, context }, use) => { const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`; const wellKnown = { - "m.homeserver": { - base_url: homeserver.baseUrl, - }, + ...config.default_server_config, "org.matrix.msc2965.authentication": { issuer, account: `${issuer}account`, @@ -57,9 +55,7 @@ export const test = base.extend<{ await route.fulfill({ json: wellKnown }); }); - await use({ - default_server_config: wellKnown, - }); + await use(context); }, }); diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 20fa6802298..7a779247213 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -21,12 +21,11 @@ test.describe("Email Registration", async () => { SMTP_PORT: mailhog.instance.smtpPort, }, }), - config: ({ homeserver }, use) => + config: ({ config }, use) => use({ + ...config, default_server_config: { - "m.homeserver": { - base_url: homeserver.baseUrl, - }, + ...config.default_server_config, "m.identity_server": { base_url: "https://server.invalid", }, diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 4f85fd61ee1..39dc538861c 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -29,18 +29,8 @@ import { Webserver } from "./plugins/webserver"; // See https://playwright.dev/docs/service-workers-experimental#how-to-enable process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1"; +// This is deliberately quite a minimal config.json, so that we can test that the default settings actually work. const CONFIG_JSON: Partial = { - // This is deliberately quite a minimal config.json, so that we can test that the default settings - // actually work. - // - // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. - // We point that to a guaranteed-invalid domain. - default_server_config: { - "m.homeserver": { - base_url: "https://server.invalid", - }, - }, - // The default language is set here for test consistency setting_defaults: { language: "en-GB", @@ -133,10 +123,18 @@ export const test = base.extend({ ); await use(context); }, - config: CONFIG_JSON, - page: async ({ context, page, config, labsFlags }, use) => { + config: {}, // We merge this atop the default CONFIG_JSON in the page fixture to make extending it easier + page: async ({ homeserver, context, page, config, labsFlags }, use) => { await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = { ...CONFIG_JSON, ...config }; + const json = { + ...CONFIG_JSON, + default_server_config: { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + }, + ...config, + }; json["features"] = { ...json["features"], // Enable the lab features From 4cacb832b07c44d682eed8d54289a8a189031a5d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 15:45:44 +0000 Subject: [PATCH 21/93] Fix mas run Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/plugins/matrix-authentication-service/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playwright/plugins/matrix-authentication-service/index.ts b/playwright/plugins/matrix-authentication-service/index.ts index d752b92e524..775497ed968 100644 --- a/playwright/plugins/matrix-authentication-service/index.ts +++ b/playwright/plugins/matrix-authentication-service/index.ts @@ -20,7 +20,7 @@ import { Instance as MailhogInstance } from "../mailhog"; // Docker tag to use for `ghcr.io/matrix-org/matrix-authentication-service` image. const TAG = "0.12.0"; -export interface ProxyInstance { +interface Instance { containerId: string; postgresId: string; configDir: string; @@ -61,7 +61,7 @@ async function cfgDirFromTemplate(opts: { export class MatrixAuthenticationService { private readonly masDocker = new Docker(); private readonly postgresDocker = new PostgresDocker("mas"); - private instance: ProxyInstance; + private instance: Instance; public port: number; constructor(private context: BrowserContext) {} @@ -71,7 +71,7 @@ export class MatrixAuthenticationService { return { port: this.port }; } - async start(homeserver: HomeserverInstance, mailhog: MailhogInstance): Promise { + async start(homeserver: HomeserverInstance, mailhog: MailhogInstance): Promise { console.log(new Date(), "Starting mas..."); if (!this.port) await this.prepare(); @@ -89,7 +89,7 @@ export class MatrixAuthenticationService { image: "ghcr.io/element-hq/matrix-authentication-service:" + TAG, containerName: "react-sdk-playwright-mas", params: ["-p", `${port}:8080/tcp`, "-v", `${configDir}:/config`], - cmd: ["mas-cli", "server", "--config", "/config/config.yaml"], + cmd: ["server", "--config", "/config/config.yaml"], }); console.log(new Date(), "started!"); From 08bb07e68081261ce0a90d452db77c925a731bb2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 15:47:23 +0000 Subject: [PATCH 22/93] break cycle Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/dehydration.spec.ts | 4 ++-- playwright/e2e/oidc/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index aa4df542a56..158e85a476e 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -17,7 +17,7 @@ const test = base.extend({ startHomeserverOpts: async ({}, use) => { await use("dehydration"); }, - context: async ({ config, context }, use) => { + config: async ({ config, context }, use) => { const wellKnown = { ...config.default_server_config, "org.matrix.msc3814": true, @@ -27,7 +27,7 @@ const test = base.extend({ await route.fulfill({ json: wellKnown }); }); - await use(context); + await use(config); }, }); diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index cb6b71b455c..79e87a3741a 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -40,7 +40,7 @@ export const test = base.extend<{ }, }); }, - context: async ({ config, startHomeserverOpts, context }, use) => { + config: async ({ config, startHomeserverOpts, context }, use) => { const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`; const wellKnown = { ...config.default_server_config, @@ -55,7 +55,7 @@ export const test = base.extend<{ await route.fulfill({ json: wellKnown }); }); - await use(context); + await use(config); }, }); From f6ea850027c316e8692dbd3414058981ce67d1a8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 16:05:47 +0000 Subject: [PATCH 23/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/complete-security.spec.ts | 9 --------- playwright/e2e/register/email.spec.ts | 2 +- playwright/e2e/register/register.spec.ts | 9 +++++++++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/playwright/e2e/crypto/complete-security.spec.ts b/playwright/e2e/crypto/complete-security.spec.ts index 9d85f85356e..0f60e172309 100644 --- a/playwright/e2e/crypto/complete-security.spec.ts +++ b/playwright/e2e/crypto/complete-security.spec.ts @@ -12,15 +12,6 @@ import { logIntoElement } from "./utils"; test.describe("Complete security", () => { test.use({ displayName: "Jeff", - config: { - // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. - // We point that to a guaranteed-invalid domain. - default_server_config: { - "m.homeserver": { - base_url: "https://server.invalid", - }, - }, - }, }); test("should go straight to the welcome screen if we have no signed device", async ({ diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 7a779247213..58e68c958e2 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -33,7 +33,7 @@ test.describe("Email Registration", async () => { }), }); - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ homeserver, page }) => { await page.goto("/#/register"); }); diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index c1b70de6947..3aea1b2d5c1 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -11,6 +11,15 @@ import { test, expect } from "../../element-web-test"; test.describe("Registration", () => { test.use({ startHomeserverOpts: "consent", + config: { + // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. + // We point that to a guaranteed-invalid domain. + default_server_config: { + "m.homeserver": { + base_url: "https://server.invalid", + }, + }, + }, }); test.beforeEach(async ({ page }) => { From b2fb036f6288f2a8a39d6b6032a0f744684965ac Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 16:06:02 +0000 Subject: [PATCH 24/93] typo Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/complete-security.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/e2e/crypto/complete-security.spec.ts b/playwright/e2e/crypto/complete-security.spec.ts index 0f60e172309..da6974459c9 100644 --- a/playwright/e2e/crypto/complete-security.spec.ts +++ b/playwright/e2e/crypto/complete-security.spec.ts @@ -23,5 +23,5 @@ test.describe("Complete security", () => { await expect(page.getByText("Welcome Jeff", { exact: true })).toBeVisible(); }); - // see also "Verify device during login with SAS" in `verifiction.spec.ts`. + // see also "Verify device during login with SAS" in `verification.spec.ts`. }); From 1368dc05642cd997b2a0e8c94009d8262f3c7f2b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 16:51:26 +0000 Subject: [PATCH 25/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/element-web-test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 39dc538861c..e0d489f3851 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -128,12 +128,13 @@ export const test = base.extend({ await context.route(`http://localhost:8080/config.json*`, async (route) => { const json = { ...CONFIG_JSON, + ...config, default_server_config: { + ...config.default_server_config, "m.homeserver": { base_url: homeserver.baseUrl, }, }, - ...config, }; json["features"] = { ...json["features"], From 27652d0620ff4c16443aef58297354698f720577 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 17:23:35 +0000 Subject: [PATCH 26/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/element-web-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index e0d489f3851..be84fbe1451 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -130,10 +130,10 @@ export const test = base.extend({ ...CONFIG_JSON, ...config, default_server_config: { - ...config.default_server_config, "m.homeserver": { base_url: homeserver.baseUrl, }, + ...config.default_server_config, }, }; json["features"] = { From 9ba9f1cd80ecc22e155be1330ad978c8d26eac6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 17:30:06 +0000 Subject: [PATCH 27/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/register/email.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 3ed17b69d09..50cfb4297ae 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -14,7 +14,7 @@ test.describe("Email Registration", async () => { test.skip(isDendrite, "not yet wired up"); test.use(emailHomeserver); test.use({ - config: ({ homeserver }, use) => + config: ({ config }, use) => use({ ...config, default_server_config: { From ea6132084e6f295d077b789fee6e7a70cdc9cf61 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 17:32:40 +0000 Subject: [PATCH 28/93] prettier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups.spec.ts | 67 +++++++++++++-------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index 508908584e4..6e6c3e04917 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -56,54 +56,49 @@ test.describe("Key backup reset from elsewhere", () => { test.use(masHomeserver); test.skip(isDendrite, "does not yet support MAS"); - test( - "Key backup is disabled when reset from elsewhere", - async ({ page, mailhogClient, request, homeserver }) => { - const testUsername = "alice"; - const testPassword = "Pa$sW0rD!"; + test("Key backup is disabled when reset from elsewhere", async ({ page, mailhogClient, request, homeserver }) => { + const testUsername = "alice"; + const testPassword = "Pa$sW0rD!"; - // there's a delay before keys are uploaded so the error doesn't appear immediately: use a fake - // clock so we can skip the delay - await page.clock.install(); + // there's a delay before keys are uploaded so the error doesn't appear immediately: use a fake + // clock so we can skip the delay + await page.clock.install(); - await page.goto("/#/login"); - await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, testUsername, "alice@email.com", testPassword); + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await registerAccountMas(page, mailhogClient, testUsername, "alice@email.com", testPassword); - await page.getByRole("button", { name: "Add room" }).click(); - await page.getByRole("menuitem", { name: "New room" }).click(); - await page.getByRole("textbox", { name: "Name" }).fill("test room"); - await page.getByRole("button", { name: "Create room" }).click(); + await page.getByRole("button", { name: "Add room" }).click(); + await page.getByRole("menuitem", { name: "New room" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("test room"); + await page.getByRole("button", { name: "Create room" }).click(); - // @ts-ignore - this runs in the browser scope where mxMatrixClientPeg is a thing. Here, it is not. - const accessToken = await page.evaluate(() => mxMatrixClientPeg.get().getAccessToken()); + // @ts-ignore - this runs in the browser scope where mxMatrixClientPeg is a thing. Here, it is not. + const accessToken = await page.evaluate(() => mxMatrixClientPeg.get().getAccessToken()); - const csAPI = new TestClientServerAPI(request, homeserver, accessToken); + const csAPI = new TestClientServerAPI(request, homeserver, accessToken); - const backupInfo = await csAPI.getCurrentBackupInfo(); + const backupInfo = await csAPI.getCurrentBackupInfo(); - await csAPI.deleteBackupVersion(backupInfo.version); + await csAPI.deleteBackupVersion(backupInfo.version); - await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession"); - await page.getByRole("button", { name: "Send message" }).click(); + await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession"); + await page.getByRole("button", { name: "Send message" }).click(); - await page - .getByRole("textbox", { name: "Send an encrypted message…" }) - .fill("Message with broken key backup"); - await page.getByRole("button", { name: "Send message" }).click(); + await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup"); + await page.getByRole("button", { name: "Send message" }).click(); - // Should be the message we sent plus the room creation event - await expect(page.locator(".mx_EventTile")).toHaveCount(2); - await expect( - page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); + // Should be the message we sent plus the room creation event + await expect(page.locator(".mx_EventTile")).toHaveCount(2); + await expect( + page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), + ).toBeVisible(); - // Wait for it to try uploading the key - await page.clock.fastForward(20000); + // Wait for it to try uploading the key + await page.clock.fastForward(20000); - await expect(page.getByRole("heading", { level: 1, name: "New Recovery Method" })).toBeVisible(); - }, - ); + await expect(page.getByRole("heading", { level: 1, name: "New Recovery Method" })).toBeVisible(); + }); }); test.describe("Backups", () => { From 0410574b206bf9aacc851af7375ac78cd9500342 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jan 2025 18:18:13 +0000 Subject: [PATCH 29/93] Wire up basics of dendriteHomeserver Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../plugins/homeserver/dendrite/index.ts | 59 ++++++++----------- playwright/services.ts | 7 ++- .../testcontainers/HomeserverContainer.ts | 19 ++++++ playwright/testcontainers/dendrite.ts | 3 +- playwright/testcontainers/synapse.ts | 7 ++- 5 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 playwright/testcontainers/HomeserverContainer.ts diff --git a/playwright/plugins/homeserver/dendrite/index.ts b/playwright/plugins/homeserver/dendrite/index.ts index acdb45cffff..c9cb3332eb2 100644 --- a/playwright/plugins/homeserver/dendrite/index.ts +++ b/playwright/plugins/homeserver/dendrite/index.ts @@ -6,39 +6,32 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -// export const dendriteHomeserver: Fixtures & Fixtures = { -// _homeserver: async ({ request }, use) => { -// const container = new SynapseContainer(request); -// await use(container); -// -// container.withConfig({ -// oidc_providers: [ -// { -// idp_id: "test", -// idp_name: "OAuth test", -// issuer: `http://localhost:${port}/oauth`, -// authorization_endpoint: `http://localhost:${port}/oauth/auth.html`, -// // the token endpoint receives requests from synapse, -// // rather than the webapp, so needs to escape the docker container. -// token_endpoint: `http://host.testcontainers.internal:${port}/oauth/token`, -// userinfo_endpoint: `http://host.testcontainers.internal:${port}/oauth/userinfo`, -// client_id: "synapse", -// discover: false, -// scopes: ["profile"], -// skip_verification: true, -// client_auth_method: "none", -// user_mapping_provider: { -// config: { -// display_name_template: "{{ user.name }}", -// }, -// }, -// }, -// ], -// }); -// await use(container); -// server.stop(); -// }, -// }; +import { Fixtures, PlaywrightTestArgs } from "@playwright/test"; + +import { Fixtures as BaseFixtures } from "../../../element-web-test.ts"; +import { DendriteContainer, PineconeContainer } from "../../../testcontainers/dendrite.ts"; +import { Services } from "../../../services.ts"; + +type Fixture = PlaywrightTestArgs & Services & BaseFixtures; +export const dendriteHomeserver: Fixtures = { + _homeserver: async ({ request }, use) => { + const container = + process.env["PLAYWRIGHT_HOMESERVER"] === "dendrite" + ? new DendriteContainer(request) + : new PineconeContainer(request); + await use(container); + }, + homeserver: async ({ logger, network, _homeserver: homeserver }, use) => { + const container = await homeserver + .withNetwork(network) + .withNetworkAliases("homeserver") + .withLogConsumer(logger.getConsumer("dendrite")) + .start(); + + await use(container); + await container.stop(); + }, +}; export function isDendrite(): boolean { return process.env["PLAYWRIGHT_HOMESERVER"] === "dendrite" || process.env["PLAYWRIGHT_HOMESERVER"] === "pinecone"; diff --git a/playwright/services.ts b/playwright/services.ts index c05cb2d29dc..58fc21775ea 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -10,9 +10,10 @@ import mailhog from "mailhog"; import { GenericContainer, Network, StartedNetwork, StartedTestContainer, Wait } from "testcontainers"; import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql"; -import { StartedSynapseContainer, SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts"; +import { SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts"; import { ContainerLogger } from "./testcontainers/utils.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; +import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts"; export interface Services { logger: ContainerLogger; @@ -24,8 +25,8 @@ export interface Services { mailhogClient: mailhog.API; synapseConfigOptions: SynapseConfigOptions; - _homeserver: SynapseContainer; - homeserver: StartedSynapseContainer; + _homeserver: HomeserverContainer; + homeserver: StartedHomeserverContainer; mas?: StartedMatrixAuthenticationServiceContainer; } diff --git a/playwright/testcontainers/HomeserverContainer.ts b/playwright/testcontainers/HomeserverContainer.ts new file mode 100644 index 00000000000..bbe075f2c95 --- /dev/null +++ b/playwright/testcontainers/HomeserverContainer.ts @@ -0,0 +1,19 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { AbstractStartedContainer, GenericContainer } from "testcontainers"; + +import { StartedSynapseContainer } from "./synapse.ts"; +import { HomeserverInstance } from "../plugins/homeserver"; + +export interface HomeserverContainer extends GenericContainer { + withConfigField(key: string, value: any): this; + withConfig(config: Partial): this; + start(): Promise; +} + +export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {} diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index 141d96156b6..484b828b524 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -13,6 +13,7 @@ import { set } from "lodash"; import { randB64Bytes } from "../plugins/utils/rand.ts"; import { StartedSynapseContainer } from "./synapse.ts"; import { deepCopy } from "../plugins/utils/object.ts"; +import { HomeserverContainer } from "./HomeserverContainer.ts"; const DEFAULT_CONFIG = { version: 2, @@ -204,7 +205,7 @@ const DEFAULT_CONFIG = { ], }; -export class DendriteContainer extends GenericContainer { +export class DendriteContainer extends GenericContainer implements HomeserverContainer { private config: typeof DEFAULT_CONFIG; constructor( diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index a04041437f3..46a4bcb415c 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -13,8 +13,9 @@ import { set } from "lodash"; import { getFreePort } from "../plugins/utils/port.ts"; import { randB64Bytes } from "../plugins/utils/rand.ts"; -import { Credentials, HomeserverInstance } from "../plugins/homeserver"; +import { Credentials } from "../plugins/homeserver"; import { deepCopy } from "../plugins/utils/object.ts"; +import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverContainer.ts"; const TAG = "develop@sha256:17cc0a301447430624afb860276e5c13270ddeb99a3f6d1c6d519a20b1a8f650"; @@ -137,7 +138,7 @@ const DEFAULT_CONFIG = { export type SynapseConfigOptions = Partial; -export class SynapseContainer extends GenericContainer { +export class SynapseContainer extends GenericContainer implements HomeserverContainer { private config: typeof DEFAULT_CONFIG; constructor(private readonly request: APIRequestContext) { @@ -225,7 +226,7 @@ export class SynapseContainer extends GenericContainer { } } -export class StartedSynapseContainer extends AbstractStartedContainer implements HomeserverInstance { +export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { private adminToken?: string; constructor( From 40d755af6ceede0ec0f2fcdb5f7afefeaac508f1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 09:09:53 +0000 Subject: [PATCH 30/93] Run Playwright tests against Dendrite & Pinecone periodically Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/end-to-end-tests.yaml | 18 +++++--- playwright.config.ts | 36 ++++++++++++--- playwright/e2e/crypto/backups.spec.ts | 2 +- .../plugins/homeserver/dendrite/index.ts | 31 ++----------- playwright/plugins/homeserver/index.ts | 2 + playwright/services.ts | 44 ++++++++++++++++--- 6 files changed, 84 insertions(+), 49 deletions(-) diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 6afabdb1fe6..8a595044298 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -114,14 +114,20 @@ jobs: - Chrome - Firefox - WebKit + - Dendrite + - Pinecone isCron: - ${{ github.event_name == 'schedule' }} - # Skip the Firefox & Safari runs unless this was a cron trigger - exclude: - - isCron: false - project: Firefox - - isCron: false - project: WebKit + # Skip the non-Chrome runs unless this was a cron trigger + # exclude: + # - isCron: false + # project: Firefox + # - isCron: false + # project: WebKit + # - isCron: false + # project: Dendrite + # - isCron: false + # project: Pinecone steps: - uses: actions/checkout@v4 with: diff --git a/playwright.config.ts b/playwright.config.ts index d317c55a6d6..09bd07bb3b8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -8,19 +8,25 @@ Please see LICENSE files in the repository root for full details. import { defineConfig, devices } from "@playwright/test"; +import { Options } from "./playwright/services"; + const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; -export default defineConfig({ +const chromeProject = { + ...devices["Desktop Chrome"], + channel: "chromium", + permissions: ["clipboard-write", "clipboard-read", "microphone"], + launchOptions: { + args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"], + }, +}; + +export default defineConfig({ projects: [ { name: "Chrome", use: { - ...devices["Desktop Chrome"], - channel: "chromium", - permissions: ["clipboard-write", "clipboard-read", "microphone"], - launchOptions: { - args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"], - }, + ...chromeProject, }, }, { @@ -48,6 +54,22 @@ export default defineConfig({ }, ignoreSnapshots: true, }, + { + name: "Dendrite", + use: { + ...chromeProject, + homeserverType: "dendrite", + }, + ignoreSnapshots: true, + }, + { + name: "Pinecone", + use: { + ...chromeProject, + homeserverType: "pinecone", + }, + ignoreSnapshots: true, + }, ], use: { viewport: { width: 1280, height: 720 }, diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index 6e6c3e04917..adb7db03aa5 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -23,7 +23,7 @@ async function expectBackupVersionToBe(page: Page, version: string) { } // These tests register an account with MAS because then we go through the "normal" registration flow -// and crypto gets set up. Using the 'user' fixture create a a user an synthesizes an existing login, +// and crypto gets set up. Using the 'user' fixture create a user and synthesizes an existing login, // which is faster but leaves us without crypto set up. test.describe("Encryption state after registration", () => { test.use(masHomeserver); diff --git a/playwright/plugins/homeserver/dendrite/index.ts b/playwright/plugins/homeserver/dendrite/index.ts index c9cb3332eb2..fb3537f4170 100644 --- a/playwright/plugins/homeserver/dendrite/index.ts +++ b/playwright/plugins/homeserver/dendrite/index.ts @@ -6,33 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { Fixtures, PlaywrightTestArgs } from "@playwright/test"; +import { Options } from "../../../services.ts"; -import { Fixtures as BaseFixtures } from "../../../element-web-test.ts"; -import { DendriteContainer, PineconeContainer } from "../../../testcontainers/dendrite.ts"; -import { Services } from "../../../services.ts"; - -type Fixture = PlaywrightTestArgs & Services & BaseFixtures; -export const dendriteHomeserver: Fixtures = { - _homeserver: async ({ request }, use) => { - const container = - process.env["PLAYWRIGHT_HOMESERVER"] === "dendrite" - ? new DendriteContainer(request) - : new PineconeContainer(request); - await use(container); - }, - homeserver: async ({ logger, network, _homeserver: homeserver }, use) => { - const container = await homeserver - .withNetwork(network) - .withNetworkAliases("homeserver") - .withLogConsumer(logger.getConsumer("dendrite")) - .start(); - - await use(container); - await container.stop(); - }, +export const isDendrite = ({ homeserverType }: Options): boolean => { + return homeserverType === "dendrite" || homeserverType === "pinecone"; }; - -export function isDendrite(): boolean { - return process.env["PLAYWRIGHT_HOMESERVER"] === "dendrite" || process.env["PLAYWRIGHT_HOMESERVER"] === "pinecone"; -} diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index c05a5c345e8..ba8a5bdb28a 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -42,3 +42,5 @@ export interface Credentials { password: string | null; // null for password-less users displayName?: string; } + +export type HomeserverType = "synapse" | "dendrite" | "pinecone"; diff --git a/playwright/services.ts b/playwright/services.ts index 58fc21775ea..1c34898a945 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -14,6 +14,8 @@ import { SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse import { ContainerLogger } from "./testcontainers/utils.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts"; +import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts"; +import { HomeserverType } from "./plugins/homeserver"; export interface Services { logger: ContainerLogger; @@ -24,13 +26,17 @@ export interface Services { mailhog: StartedTestContainer; mailhogClient: mailhog.API; - synapseConfigOptions: SynapseConfigOptions; _homeserver: HomeserverContainer; homeserver: StartedHomeserverContainer; mas?: StartedMatrixAuthenticationServiceContainer; } -export const test = base.extend({ +export interface Options { + synapseConfigOptions: SynapseConfigOptions; + homeserverType: HomeserverType; +} + +export const test = base.extend({ // eslint-disable-next-line no-empty-pattern logger: async ({}, use, testInfo) => { const logger = new ContainerLogger(); @@ -85,16 +91,40 @@ export const test = base.extend({ }, synapseConfigOptions: [{}, { option: true }], - _homeserver: async ({ request }, use) => { - const container = new SynapseContainer(request); + homeserverType: ["synapse", { option: true }], + _homeserver: async ({ homeserverType, request }, use) => { + let container: HomeserverContainer; + switch (homeserverType) { + case "synapse": + container = new SynapseContainer(request); + break; + case "dendrite": + container = new DendriteContainer(request); + break; + case "pinecone": + container = new PineconeContainer(request); + break; + } + await use(container); }, - homeserver: async ({ logger, network, _homeserver: homeserver, synapseConfigOptions, mas }, use) => { + homeserver: async ( + { homeserverType, logger, network, _homeserver: homeserver, synapseConfigOptions, mas }, + use, + ) => { + test.skip( + !(homeserver instanceof SynapseContainer) && Object.keys(synapseConfigOptions).length > 0, + `Test specifies Synapse config options so is unsupported with ${homeserverType}`, + ); + + if (homeserver instanceof SynapseContainer) { + homeserver.withConfig(synapseConfigOptions); + } + const container = await homeserver .withNetwork(network) .withNetworkAliases("homeserver") - .withLogConsumer(logger.getConsumer("synapse")) - .withConfig(synapseConfigOptions) + .withLogConsumer(logger.getConsumer(homeserverType)) .start(); await use(container); From df510dc74b01470756c43476019e9fb26a6c3f97 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 09:55:30 +0000 Subject: [PATCH 31/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/end-to-end-tests.yaml | 6 +++--- playwright/e2e/knock/knock-into-room.spec.ts | 2 ++ playwright/e2e/knock/manage-knocks.spec.ts | 2 ++ playwright/flaky-reporter.ts | 10 +++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 8a595044298..5f9bab029a4 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -111,9 +111,9 @@ jobs: # Run multiple instances in parallel to speed up the tests runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }} project: - - Chrome - - Firefox - - WebKit + # - Chrome + # - Firefox + # - WebKit - Dendrite - Pinecone isCron: diff --git a/playwright/e2e/knock/knock-into-room.spec.ts b/playwright/e2e/knock/knock-into-room.spec.ts index a87c33415b3..9c2f1ee76be 100644 --- a/playwright/e2e/knock/knock-into-room.spec.ts +++ b/playwright/e2e/knock/knock-into-room.spec.ts @@ -13,8 +13,10 @@ import { type Visibility } from "matrix-js-sdk/src/matrix"; import { test, expect } from "../../element-web-test"; import { waitForRoom } from "../utils"; import { Filter } from "../../pages/Spotlight"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Knock Into Room", () => { + test.skip(isDendrite, "Dendrite does not have support for knocking"); test.use({ displayName: "Alice", labsFlags: ["feature_ask_to_join"], diff --git a/playwright/e2e/knock/manage-knocks.spec.ts b/playwright/e2e/knock/manage-knocks.spec.ts index fb7e2751945..870656398e6 100644 --- a/playwright/e2e/knock/manage-knocks.spec.ts +++ b/playwright/e2e/knock/manage-knocks.spec.ts @@ -10,8 +10,10 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { waitForRoom } from "../utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Manage Knocks", () => { + test.skip(isDendrite, "Dendrite does not have support for knocking"); test.use({ displayName: "Alice", labsFlags: ["feature_ask_to_join"], diff --git a/playwright/flaky-reporter.ts b/playwright/flaky-reporter.ts index ad92aca12e5..2a47f40a013 100644 --- a/playwright/flaky-reporter.ts +++ b/playwright/flaky-reporter.ts @@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details. * Only intended to run from within GitHub Actions */ -import type { Reporter, TestCase } from "@playwright/test/reporter"; +import type { Reporter, Suite, TestCase, FullConfig } from "@playwright/test/reporter"; const REPO = "element-hq/element-web"; const LABEL = "Z-Flaky-Test"; @@ -26,8 +26,16 @@ type PaginationLinks = { class FlakyReporter implements Reporter { private flakes = new Set(); + private ignoreSuite = false; + + public onBegin(config: FullConfig, suite: Suite) { + const projectName = suite.project().name; + // Ignores flakes on Dendrite and Pinecone as they have their own flakes we do not track + this.ignoreSuite = ["Dendrite", "Pinecone"].includes(projectName); + } public onTestEnd(test: TestCase): void { + if (this.ignoreSuite) return; const title = `${test.location.file.split("playwright/e2e/")[1]}: ${test.title}`; if (test.outcome() === "flaky") { this.flakes.add(title); From 7d8f00f404afdd262b51b4c1b7714615edb0b31c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 10:46:40 +0000 Subject: [PATCH 32/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/complete-security.spec.ts | 2 ++ playwright/e2e/crypto/crypto.spec.ts | 2 ++ playwright/e2e/login/soft_logout.spec.ts | 2 ++ playwright/e2e/register/register.spec.ts | 3 +++ playwright/e2e/spotlight/spotlight.spec.ts | 3 +++ 5 files changed, 12 insertions(+) diff --git a/playwright/e2e/crypto/complete-security.spec.ts b/playwright/e2e/crypto/complete-security.spec.ts index da6974459c9..d4c303fae44 100644 --- a/playwright/e2e/crypto/complete-security.spec.ts +++ b/playwright/e2e/crypto/complete-security.spec.ts @@ -8,8 +8,10 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { logIntoElement } from "./utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Complete security", () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.use({ displayName: "Jeff", }); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 2b6844574ec..15453a408e0 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -11,6 +11,7 @@ import { expect, test } from "../../element-web-test"; import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils"; import { Bot } from "../../pages/bot"; import { ElementAppPage } from "../../pages/ElementAppPage"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const checkDMRoom = async (page: Page) => { const body = page.locator(".mx_RoomView_body"); @@ -77,6 +78,7 @@ test.describe("Cryptography", function () { for (const isDeviceVerified of [true, false]) { test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); /** * Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server * @param keyType diff --git a/playwright/e2e/login/soft_logout.spec.ts b/playwright/e2e/login/soft_logout.spec.ts index 6b4aa5a40d8..89060c4da4f 100644 --- a/playwright/e2e/login/soft_logout.spec.ts +++ b/playwright/e2e/login/soft_logout.spec.ts @@ -12,6 +12,7 @@ import { test, expect } from "../../element-web-test"; import { doTokenRegistration } from "./utils"; import { Credentials } from "../../plugins/homeserver"; import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Soft logout", () => { test.use({ @@ -53,6 +54,7 @@ test.describe("Soft logout", () => { }); test.describe("with SSO user", () => { + test.skip(isDendrite, "does not yet support SSO"); test.use(legacyOAuthHomeserver); test.use({ user: async ({ page, homeserver }, use) => { diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index 7756fe53c5a..a01b9aa760c 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Registration", () => { test.use(consentHomeserver); @@ -31,6 +32,8 @@ test.describe("Registration", () => { "registers an account and lands on the home screen", { tag: "@screenshot" }, async ({ homeserver, page, checkA11y, crypto }) => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); + await page.getByRole("button", { name: "Edit", exact: true }).click(); await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index d1bb3dec258..1b3b724ce04 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -12,6 +12,7 @@ import { Filter } from "../../pages/Spotlight"; import { Bot } from "../../pages/bot"; import type { Locator, Page } from "@playwright/test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; function roomHeaderName(page: Page): Locator { return page.locator(".mx_RoomHeader_heading"); @@ -39,6 +40,8 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise { + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3123"); + const bot1Name = "BotBob"; let bot1: Bot; From e4cff3fba013d2444d95c2ef51cff3e119ad0640 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 11:18:26 +0000 Subject: [PATCH 33/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups.spec.ts | 1 + playwright/e2e/crypto/logout.spec.ts | 2 ++ playwright/e2e/login/login.spec.ts | 1 + playwright/e2e/register/register.spec.ts | 3 +-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index adb7db03aa5..93da85b9b80 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -102,6 +102,7 @@ test.describe("Key backup reset from elsewhere", () => { }); test.describe("Backups", () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.use({ displayName: "Hanako", }); diff --git a/playwright/e2e/crypto/logout.spec.ts b/playwright/e2e/crypto/logout.spec.ts index 2bafe0ece88..faaf1e6a1e3 100644 --- a/playwright/e2e/crypto/logout.spec.ts +++ b/playwright/e2e/crypto/logout.spec.ts @@ -8,8 +8,10 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { createRoom, enableKeyBackup, logIntoElement, sendMessageInCurrentRoom } from "./utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Logout tests", () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.beforeEach(async ({ page, homeserver, credentials }) => { await logIntoElement(page, credentials); }); diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login.spec.ts index 24b23fdbd8d..fd879d77cf7 100644 --- a/playwright/e2e/login/login.spec.ts +++ b/playwright/e2e/login/login.spec.ts @@ -93,6 +93,7 @@ test.describe("Login", () => { }); test.describe("Password login", () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.use(consentHomeserver); let creds: Credentials; diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index a01b9aa760c..5c842a363fa 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -11,6 +11,7 @@ import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomes import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Registration", () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.use(consentHomeserver); test.use({ config: { @@ -32,8 +33,6 @@ test.describe("Registration", () => { "registers an account and lands on the home screen", { tag: "@screenshot" }, async ({ homeserver, page, checkA11y, crypto }) => { - test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); - await page.getByRole("button", { name: "Edit", exact: true }).click(); await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); From 4142c809a39fb6a59560fca14b2c86a4bef303df Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 13:38:50 +0000 Subject: [PATCH 34/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/integration-manager/kick.spec.ts | 26 +++++-------------- playwright/pages/bot.ts | 2 +- playwright/stale-screenshot-reporter.ts | 1 + src/TextForEvent.tsx | 5 ++-- .../views/messages/TextualEvent.tsx | 16 +++++++++++- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/playwright/e2e/integration-manager/kick.spec.ts b/playwright/e2e/integration-manager/kick.spec.ts index b12215fe332..acd50bfbd4a 100644 --- a/playwright/e2e/integration-manager/kick.spec.ts +++ b/playwright/e2e/integration-manager/kick.spec.ts @@ -69,29 +69,15 @@ async function sendActionFromIntegrationManager( await iframe.getByRole("button", { name: "Press to send action" }).click(); } -async function clickUntilGone(page: Page, selector: string, attempt = 0) { - if (attempt === 11) { - throw new Error("clickUntilGone attempt count exceeded"); - } - - await page.locator(selector).last().click(); - - const count = await page.locator(selector).count(); - if (count > 0) { - return clickUntilGone(page, selector, ++attempt); - } -} - async function expectKickedMessage(page: Page, shouldExist: boolean) { - // Expand any event summaries, we can't use a click multiple here because clicking one might de-render others - // This is quite horrible but seems the most stable way of clicking 0-N buttons, - // one at a time with a full re-evaluation after each click - await clickUntilGone(page, ".mx_GenericEventListSummary_toggle[aria-expanded=false]"); + await expect(async () => { + await page.locator(".mx_GenericEventListSummary_toggle[aria-expanded=false]").last().click(); + await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({ + visible: shouldExist, + }); + }).toPass(); // Check for the event message (or lack thereof) - await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({ - visible: shouldExist, - }); } test.describe("Integration Manager: Kick", () => { diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index 1d414c7bf6a..159d7fcbc2e 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -121,7 +121,7 @@ export class Bot extends Client { return logger as unknown as Logger; } - const logger = getLogger(`cypress bot ${credentials.userId}`); + const logger = getLogger(`playwright bot ${credentials.userId}`); const keys = {}; diff --git a/playwright/stale-screenshot-reporter.ts b/playwright/stale-screenshot-reporter.ts index 3e38f78ca93..dc934827c1d 100644 --- a/playwright/stale-screenshot-reporter.ts +++ b/playwright/stale-screenshot-reporter.ts @@ -23,6 +23,7 @@ class StaleScreenshotReporter implements Reporter { private success = true; public onTestEnd(test: TestCase): void { + if (!test.ok()) return; for (const annotation of test.annotations) { if (annotation.type === "_screenshot") { this.screenshots.add(annotation.description); diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index bdb7e8cbe0e..1194e4ca716 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -107,9 +107,8 @@ function textForMemberEvent( allowJSX: boolean, showHiddenEvents?: boolean, ): (() => string) | null { - // XXX: SYJS-16 "sender is sometimes null for join messages" - const senderName = ev.sender?.name || getRoomMemberDisplayname(client, ev); - const targetName = ev.target?.name || getRoomMemberDisplayname(client, ev, ev.getStateKey()); + const senderName = getRoomMemberDisplayname(client, ev); + const targetName = getRoomMemberDisplayname(client, ev, ev.getStateKey()); const prevContent = ev.getPrevContent(); const content = ev.getContent(); const reason = content.reason; diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index 8549fc5cab5..831aaee1175 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix"; import RoomContext from "../../../contexts/RoomContext"; import * as TextForEvent from "../../../TextForEvent"; @@ -21,6 +21,20 @@ export default class TextualEvent extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; + public componentDidMount(): void { + MatrixClientPeg.get()?.on(RoomMemberEvent.Name, this.onMemberNameUpdate); + } + + public componentWillUnmount(): void { + MatrixClientPeg.get()?.off(RoomMemberEvent.Name, this.onMemberNameUpdate); + } + + private onMemberNameUpdate = (event: MatrixEvent, member: RoomMember): void => { + if (member.userId === this.props.mxEvent.getSender() || member.userId === this.props.mxEvent.getStateKey()) { + this.forceUpdate(); + } + }; + public render(): React.ReactNode { const text = TextForEvent.textForEvent( this.props.mxEvent, From 5e304beedd35f1c5a590c665c492f5405aa86e97 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 14:41:47 +0000 Subject: [PATCH 35/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/create-room/create-room.spec.ts | 9 +++++++-- playwright/e2e/editing/editing.spec.ts | 3 +++ playwright/e2e/knock/create-knock-room.spec.ts | 2 ++ playwright/e2e/right-panel/file-panel.spec.ts | 3 +++ playwright/e2e/widgets/stickers.spec.ts | 13 +++++++------ playwright/testcontainers/synapse.ts | 4 ++-- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/playwright/e2e/create-room/create-room.spec.ts b/playwright/e2e/create-room/create-room.spec.ts index 4a4fee16207..654352d3b8e 100644 --- a/playwright/e2e/create-room/create-room.spec.ts +++ b/playwright/e2e/create-room/create-room.spec.ts @@ -11,7 +11,12 @@ import { test, expect } from "../../element-web-test"; test.describe("Create Room", () => { test.use({ displayName: "Jim" }); - test("should allow us to create a public room with name, topic & address set", async ({ page, user, app }) => { + test("should allow us to create a public room with name, topic & address set", async ({ + credentials, + page, + user, + app, + }) => { const name = "Test room 1"; const topic = "This room is dedicated to this test and this test only!"; @@ -27,7 +32,7 @@ test.describe("Create Room", () => { // Submit await dialog.getByRole("button", { name: "Create room" }).click(); - await expect(page).toHaveURL(/\/#\/room\/#test-room-1:localhost/); + await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${credentials.homeServer}`)); const header = page.locator(".mx_RoomHeader"); await expect(header).toContainText(name); }); diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts index 83ae6ba2d9c..6fc9c495989 100644 --- a/playwright/e2e/editing/editing.spec.ts +++ b/playwright/e2e/editing/editing.spec.ts @@ -12,6 +12,7 @@ import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } fro import { expect, test } from "../../element-web-test"; import { ElementAppPage } from "../../pages/ElementAppPage"; import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; async function sendEvent(app: ElementAppPage, roomId: string): Promise { return app.client.sendEvent(roomId, null, "m.room.message" as EventType, { @@ -31,6 +32,8 @@ function mkPadding(n: number): IContent { } test.describe("Editing", () => { + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3123"); + // Edit "Message" const editLastMessage = async (page: Page, edit: string) => { const eventTile = page.locator(".mx_RoomView_MessageList .mx_EventTile_last"); diff --git a/playwright/e2e/knock/create-knock-room.spec.ts b/playwright/e2e/knock/create-knock-room.spec.ts index 1c729ff610e..29733481ddc 100644 --- a/playwright/e2e/knock/create-knock-room.spec.ts +++ b/playwright/e2e/knock/create-knock-room.spec.ts @@ -9,8 +9,10 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { waitForRoom } from "../utils"; import { Filter } from "../../pages/Spotlight"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Create Knock Room", () => { + test.skip(isDendrite, "Dendrite does not have support for knocking"); test.use({ displayName: "Alice", labsFlags: ["feature_ask_to_join"], diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index 579ba05bb7f..1c936f43b89 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -10,6 +10,7 @@ import { Download, type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { viewRoomSummaryByName } from "./utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const ROOM_NAME = "Test room"; const NAME = "Alice"; @@ -181,6 +182,8 @@ test.describe("FilePanel", () => { }); test.describe("download", () => { + test.skip(isDendrite, "due to a Dendrite sending Content-Disposition inline"); + test("should download an image via the link on the panel", async ({ page, context }) => { // Upload an image file await uploadFile(page, "playwright/sample-files/riot.png"); diff --git a/playwright/e2e/widgets/stickers.spec.ts b/playwright/e2e/widgets/stickers.spec.ts index 54de1b69e28..5820f2c1697 100644 --- a/playwright/e2e/widgets/stickers.spec.ts +++ b/playwright/e2e/widgets/stickers.spec.ts @@ -88,7 +88,7 @@ async function sendStickerFromPicker(page: Page) { await expect(page.locator(".mx_AppTileFullWidth#stickers")).not.toBeVisible(); } -async function expectTimelineSticker(page: Page, roomId: string, contentUri: string) { +async function expectTimelineSticker(page: Page, serverName: string, roomId: string, contentUri: string) { const contentId = contentUri.split("/").slice(-1)[0]; // Make sure it's in the right room await expect(page.locator(".mx_EventTile_sticker > a")).toHaveAttribute("href", new RegExp(`/${roomId}/`)); @@ -98,7 +98,7 @@ async function expectTimelineSticker(page: Page, roomId: string, contentUri: str // download URL. await expect(page.locator(`img[alt="${STICKER_NAME}"]`)).toHaveAttribute( "src", - new RegExp(`/localhost/${contentId}`), + new RegExp(`/${serverName}/${contentId}`), ); } @@ -145,7 +145,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { // See sendStickerFromPicker() for more detail on iframe comms. let stickerPickerUrl: string; - test("should send a sticker to multiple rooms", async ({ webserver, page, app, user, room }) => { + test("should send a sticker to multiple rooms", async ({ credentials, webserver, page, app, user, room }) => { const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 }); const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); const widgetHtml = getWidgetHtml(contentUri, "image/png"); @@ -156,7 +156,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(page).toHaveURL(`/#/room/${room.roomId}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, room.roomId, contentUri); + await expectTimelineSticker(page, credentials.homeServer, room.roomId, contentUri); // Ensure that when we switch to a different room that the sticker // goes to the right place @@ -164,7 +164,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(page).toHaveURL(`/#/room/${roomId2}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, roomId2, contentUri); + await expectTimelineSticker(page, credentials.homeServer, roomId2, contentUri); }); test("should handle a sticker picker widget missing creatorUserId", async ({ @@ -173,6 +173,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { app, user, room, + credentials, }) => { const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); const widgetHtml = getWidgetHtml(contentUri, "image/png"); @@ -183,7 +184,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(page).toHaveURL(`/#/room/${room.roomId}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, room.roomId, contentUri); + await expectTimelineSticker(page, credentials.homeServer, room.roomId, contentUri); }); test("should render invalid mimetype as a file", async ({ webserver, page, app, user, room }) => { diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 46a4bcb415c..b547da5dfdf 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -267,7 +267,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements const data = await res.json(); return { - homeServer: data.home_server, + homeServer: data.home_server || data.user_id.split(":").slice(1).join(":"), accessToken: data.access_token, userId: data.user_id, deviceId: data.device_id, @@ -299,7 +299,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements accessToken: json.access_token, userId: json.user_id, deviceId: json.device_id, - homeServer: json.home_server, + homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"), }; } From 18a3e720aae3248bbab554a71d1439cd59240257 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 15:08:12 +0000 Subject: [PATCH 36/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/create-room/create-room.spec.ts | 9 +-- playwright/e2e/crypto/crypto.spec.ts | 2 +- .../e2e/lazy-loading/lazy-loading.spec.ts | 25 +++++++-- playwright/e2e/permalinks/permalinks.spec.ts | 2 +- .../e2e/right-panel/right-panel.spec.ts | 50 +++++++++-------- .../e2e/room-directory/room-directory.spec.ts | 6 +- .../general-room-settings-tab.spec.ts | 4 +- playwright/e2e/spaces/spaces.spec.ts | 6 +- .../spaces/threads-activity-centre/index.ts | 7 ++- .../threadsActivityCentre.spec.ts | 55 +++++++++++-------- playwright/e2e/widgets/stickers.spec.ts | 9 ++- playwright/element-web-test.ts | 2 +- 12 files changed, 100 insertions(+), 77 deletions(-) diff --git a/playwright/e2e/create-room/create-room.spec.ts b/playwright/e2e/create-room/create-room.spec.ts index 654352d3b8e..087a89e68dc 100644 --- a/playwright/e2e/create-room/create-room.spec.ts +++ b/playwright/e2e/create-room/create-room.spec.ts @@ -11,12 +11,7 @@ import { test, expect } from "../../element-web-test"; test.describe("Create Room", () => { test.use({ displayName: "Jim" }); - test("should allow us to create a public room with name, topic & address set", async ({ - credentials, - page, - user, - app, - }) => { + test("should allow us to create a public room with name, topic & address set", async ({ page, user, app }) => { const name = "Test room 1"; const topic = "This room is dedicated to this test and this test only!"; @@ -32,7 +27,7 @@ test.describe("Create Room", () => { // Submit await dialog.getByRole("button", { name: "Create room" }).click(); - await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${credentials.homeServer}`)); + await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`)); const header = page.locator(".mx_RoomHeader"); await expect(header).toContainText(name); }); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 15453a408e0..f99a7a64588 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -68,6 +68,7 @@ const bobJoin = async (page: Page, bob: Bot) => { }; test.describe("Cryptography", function () { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.use({ displayName: "Alice", botCreateOpts: { @@ -78,7 +79,6 @@ test.describe("Cryptography", function () { for (const isDeviceVerified of [true, false]) { test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => { - test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); /** * Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server * @param keyType diff --git a/playwright/e2e/lazy-loading/lazy-loading.spec.ts b/playwright/e2e/lazy-loading/lazy-loading.spec.ts index 6c9457dfafb..23ccaf80858 100644 --- a/playwright/e2e/lazy-loading/lazy-loading.spec.ts +++ b/playwright/e2e/lazy-loading/lazy-loading.spec.ts @@ -10,6 +10,7 @@ import { Bot } from "../../pages/bot"; import type { Locator, Page } from "@playwright/test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; import { test, expect } from "../../element-web-test"; +import { Credentials } from "../../plugins/homeserver"; test.describe("Lazy Loading", () => { const charlies: Bot[] = []; @@ -35,12 +36,18 @@ test.describe("Lazy Loading", () => { }); const name = "Lazy Loading Test"; - const alias = "#lltest:localhost"; const charlyMsg1 = "hi bob!"; const charlyMsg2 = "how's it going??"; let roomId: string; - async function setupRoomWithBobAliceAndCharlies(page: Page, app: ElementAppPage, bob: Bot, charlies: Bot[]) { + async function setupRoomWithBobAliceAndCharlies( + page: Page, + app: ElementAppPage, + user: Credentials, + bob: Bot, + charlies: Bot[], + ) { + const alias = `#lltest:${user.homeServer}`; const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public); roomId = await bob.createRoom({ name, @@ -95,7 +102,13 @@ test.describe("Lazy Loading", () => { } } - async function joinCharliesWhileAliceIsOffline(page: Page, app: ElementAppPage, charlies: Bot[]) { + async function joinCharliesWhileAliceIsOffline( + page: Page, + app: ElementAppPage, + user: Credentials, + charlies: Bot[], + ) { + const alias = `#lltest:${user.homeServer}`; await app.client.network.goOffline(); for (const charly of charlies) { await charly.joinRoom(alias); @@ -107,19 +120,19 @@ test.describe("Lazy Loading", () => { await app.client.waitForNextSync(); } - test("should handle lazy loading properly even when offline", async ({ page, app, bot }) => { + test("should handle lazy loading properly even when offline", async ({ page, app, bot, user }) => { test.slow(); const charly1to5 = charlies.slice(0, 5); const charly6to10 = charlies.slice(5); // Set up room with alice, bob & charlies 1-5 - await setupRoomWithBobAliceAndCharlies(page, app, bot, charly1to5); + await setupRoomWithBobAliceAndCharlies(page, app, user, bot, charly1to5); // Alice should see 2 messages from every charly with the correct display name await checkPaginatedDisplayNames(app, charly1to5); await openMemberlist(app); await checkMemberList(page, charly1to5); - await joinCharliesWhileAliceIsOffline(page, app, charly6to10); + await joinCharliesWhileAliceIsOffline(page, app, user, charly6to10); await checkMemberList(page, charly6to10); for (const charly of charlies) { diff --git a/playwright/e2e/permalinks/permalinks.spec.ts b/playwright/e2e/permalinks/permalinks.spec.ts index 9b448455ecf..e7657b13946 100644 --- a/playwright/e2e/permalinks/permalinks.spec.ts +++ b/playwright/e2e/permalinks/permalinks.spec.ts @@ -31,7 +31,7 @@ test.describe("permalinks", () => { await charlotte.prepareClient(); // We don't use a bot for danielle as we want a stable MXID. - const danielleId = "@danielle:localhost"; + const danielleId = `@danielle:${user.homeServer}`; const room1Id = await app.client.createRoom({ name: room1Name }); const room2Id = await app.client.createRoom({ name: room2Name }); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 2f51f92587e..23af4c37b43 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -38,29 +38,33 @@ test.describe("RightPanel", () => { }); test.describe("in rooms", () => { - test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => { - await app.client.createRoom({ name: ROOM_NAME_LONG }); - await viewRoomSummaryByName(page, app, ROOM_NAME_LONG); - - await app.settings.openRoomSettings(); - - // Set a local room address - const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" }); - await localAddresses.getByRole("textbox").fill(ROOM_ADDRESS_LONG); - await localAddresses.getByRole("button", { name: "Add" }).click(); - await expect(localAddresses.getByText(`#${ROOM_ADDRESS_LONG}:localhost`)).toHaveClass( - "mx_EditableItem_item", - ); - - await app.closeDialog(); - - // Close and reopen the right panel to render the room address - await app.toggleRoomInfoPanel(); - await expect(page.locator(".mx_RightPanel")).not.toBeVisible(); - await app.toggleRoomInfoPanel(); - - await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-name-and-address.png"); - }); + test( + "should handle long room address and long room name", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await app.client.createRoom({ name: ROOM_NAME_LONG }); + await viewRoomSummaryByName(page, app, ROOM_NAME_LONG); + + await app.settings.openRoomSettings(); + + // Set a local room address + const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" }); + await localAddresses.getByRole("textbox").fill(ROOM_ADDRESS_LONG); + await localAddresses.getByRole("button", { name: "Add" }).click(); + await expect(localAddresses.getByText(`#${ROOM_ADDRESS_LONG}:${user.homeServer}`)).toHaveClass( + "mx_EditableItem_item", + ); + + await app.closeDialog(); + + // Close and reopen the right panel to render the room address + await app.toggleRoomInfoPanel(); + await expect(page.locator(".mx_RightPanel")).not.toBeVisible(); + await app.toggleRoomInfoPanel(); + + await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-name-and-address.png"); + }, + ); test("should handle clicking add widgets", async ({ page, app }) => { await viewRoomSummaryByName(page, app, ROOM_NAME); diff --git a/playwright/e2e/room-directory/room-directory.spec.ts b/playwright/e2e/room-directory/room-directory.spec.ts index 34004c90d27..9006328ccec 100644 --- a/playwright/e2e/room-directory/room-directory.spec.ts +++ b/playwright/e2e/room-directory/room-directory.spec.ts @@ -31,11 +31,11 @@ test.describe("Room Directory", () => { const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" }); await localAddresses.getByRole("textbox").fill("gaming"); await localAddresses.getByRole("button", { name: "Add" }).click(); - await expect(localAddresses.getByText("#gaming:localhost")).toHaveClass("mx_EditableItem_item"); + await expect(localAddresses.getByText(`#gaming:${user.homeServer}`)).toHaveClass("mx_EditableItem_item"); // Publish into the public rooms directory const publishedAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Published Addresses" }); - await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue("#gaming:localhost"); + await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue(`#gaming:${user.homeServer}`); const checkbox = publishedAddresses .locator(".mx_SettingsFlag", { hasText: "Publish this room to the public in localhost's room directory?", @@ -86,7 +86,7 @@ test.describe("Room Directory", () => { .getByRole("button", { name: "Join" }) .click(); - await expect(page).toHaveURL("/#/room/#test1234:localhost"); + await expect(page).toHaveURL(`/#/room/#test1234:${user.homeServer}`); }, ); }); diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts index eec32f7af5e..4216deca220 100644 --- a/playwright/e2e/settings/general-room-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts @@ -36,7 +36,7 @@ test.describe("General room settings tab", () => { await expect(settings.getByText("Show more")).toBeVisible(); }); - test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app }) => { + test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app, user }) => { const settings = await app.settings.openRoomSettings("General"); // 1. Set the room-address to be a really long string const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4); @@ -44,7 +44,7 @@ test.describe("General room settings tab", () => { await settings.locator("#roomAliases").getByText("Add", { exact: true }).click(); // 2. wait for the new setting to apply ... - await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:localhost`); + await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:${user.homeServer}`); // 3. Check if the dialog overflows const dialogBoundingBox = await page.locator(".mx_Dialog").boundingBox(); diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts index 48bcc13c53a..14df58fa8e6 100644 --- a/playwright/e2e/spaces/spaces.spec.ts +++ b/playwright/e2e/spaces/spaces.spec.ts @@ -82,7 +82,7 @@ test.describe("Spaces", () => { // Copy matrix.to link await page.getByRole("button", { name: "Share invite link" }).click(); - expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#lets-have-a-riot:localhost"); + expect(await app.getClipboardText()).toEqual(`https://matrix.to/#/#lets-have-a-riot:${user.homeServer}`); // Go to space home await page.getByRole("button", { name: "Go to my first room" }).click(); @@ -169,13 +169,13 @@ test.describe("Spaces", () => { room_alias_name: "space", }); - const menu = await openSpaceContextMenu(page, app, "#space:localhost"); + const menu = await openSpaceContextMenu(page, app, `#space:${user.homeServer}`); await menu.getByRole("menuitem", { name: "Invite" }).click(); const shareDialog = page.locator(".mx_SpacePublicShare"); // Copy link first await shareDialog.getByRole("button", { name: "Share invite link" }).click(); - expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#space:localhost"); + expect(await app.getClipboardText()).toEqual(`https://matrix.to/#/#space:${user.homeServer}`); // Start Matrix invite flow await shareDialog.getByRole("button", { name: "Invite people" }).click(); diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index 2555e5a8353..c4c69440b52 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -13,6 +13,7 @@ import { test as base, expect } from "../../../element-web-test"; import { Bot } from "../../../pages/bot"; import { Client } from "../../../pages/client"; import { ElementAppPage } from "../../../pages/ElementAppPage"; +import { Credentials } from "../../../plugins/homeserver"; type RoomRef = { name: string; roomId: string }; @@ -333,12 +334,14 @@ export class Helpers { /** * Populate the rooms with messages and threads + * @param user the user sending the messages * @param room1 * @param room2 * @param msg - MessageBuilder * @param hasMention - whether to include a mention in the first message */ async populateThreads( + user: Credentials, room1: { name: string; roomId: string }, room2: { name: string; roomId: string }, msg: MessageBuilder, @@ -350,9 +353,9 @@ export class Helpers { msg.threadedOff("Msg1", { "body": "User", "format": "org.matrix.custom.html", - "formatted_body": "User", + "formatted_body": `User`, "m.mentions": { - user_ids: ["@user:localhost"], + user_ids: [user.userId], }, }), ]); diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 16276c5b9db..dcdcf1a21a4 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -46,16 +46,21 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { await util.assertNotificationTac(); }); - test("should show a highlight indicator when there is a mention in a thread", async ({ room1, util, msg }) => { + test("should show a highlight indicator when there is a mention in a thread", async ({ + room1, + util, + msg, + user, + }) => { await util.goTo(room1); await util.receiveMessages(room1, [ "Msg1", msg.threadedOff("Msg1", { "body": "User", "format": "org.matrix.custom.html", - "formatted_body": "User", + "formatted_body": `User`, "m.mentions": { - user_ids: ["@user:localhost"], + user_ids: [user.userId], }, }), ]); @@ -64,26 +69,30 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { await util.assertHighlightIndicator(); }); - test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { - await util.goTo(room2); - await util.populateThreads(room1, room2, msg); - // The indicator should be shown - await util.assertHighlightIndicator(); - - // Verify that we have the expected rooms in the TAC - await util.openTac(); - await util.assertRoomsInTac([ - { room: room2.name, notificationLevel: "highlight" }, - { room: room1.name, notificationLevel: "notification" }, - ]); - - // Verify that we don't have a visual regression - await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); - }); + test( + "should show the rooms with unread threads", + { tag: "@screenshot" }, + async ({ room1, room2, util, msg, user }) => { + await util.goTo(room2); + await util.populateThreads(user, room1, room2, msg); + // The indicator should be shown + await util.assertHighlightIndicator(); + + // Verify that we have the expected rooms in the TAC + await util.openTac(); + await util.assertRoomsInTac([ + { room: room2.name, notificationLevel: "highlight" }, + { room: room1.name, notificationLevel: "notification" }, + ]); + + // Verify that we don't have a visual regression + await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); + }, + ); - test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { + test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, user }) => { await util.goTo(room2); - await util.populateThreads(room1, room2, msg); + await util.populateThreads(user, room1, room2, msg); // Click on the first room in TAC await util.openTac(); @@ -104,9 +113,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-notification-unread.png"); }); - test("should order by recency after notification level", async ({ room1, room2, util, msg }) => { + test("should order by recency after notification level", async ({ room1, room2, util, msg, user }) => { await util.goTo(room2); - await util.populateThreads(room1, room2, msg, false); + await util.populateThreads(user, room1, room2, msg, false); await util.openTac(); await util.assertRoomsInTac([ diff --git a/playwright/e2e/widgets/stickers.spec.ts b/playwright/e2e/widgets/stickers.spec.ts index 5820f2c1697..2b7b2709f7c 100644 --- a/playwright/e2e/widgets/stickers.spec.ts +++ b/playwright/e2e/widgets/stickers.spec.ts @@ -145,7 +145,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { // See sendStickerFromPicker() for more detail on iframe comms. let stickerPickerUrl: string; - test("should send a sticker to multiple rooms", async ({ credentials, webserver, page, app, user, room }) => { + test("should send a sticker to multiple rooms", async ({ webserver, page, app, user, room }) => { const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 }); const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); const widgetHtml = getWidgetHtml(contentUri, "image/png"); @@ -156,7 +156,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(page).toHaveURL(`/#/room/${room.roomId}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, credentials.homeServer, room.roomId, contentUri); + await expectTimelineSticker(page, user.homeServer, room.roomId, contentUri); // Ensure that when we switch to a different room that the sticker // goes to the right place @@ -164,7 +164,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(page).toHaveURL(`/#/room/${roomId2}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, credentials.homeServer, roomId2, contentUri); + await expectTimelineSticker(page, user.homeServer, roomId2, contentUri); }); test("should handle a sticker picker widget missing creatorUserId", async ({ @@ -173,7 +173,6 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { app, user, room, - credentials, }) => { const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); const widgetHtml = getWidgetHtml(contentUri, "image/png"); @@ -184,7 +183,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(page).toHaveURL(`/#/room/${room.roomId}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, credentials.homeServer, room.roomId, contentUri); + await expectTimelineSticker(page, user.homeServer, room.roomId, contentUri); }); test("should render invalid mimetype as a file", async ({ webserver, page, app, user, room }) => { diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index f2f2afd3b95..da0ad64eee1 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -143,7 +143,7 @@ export const test = base.extend({ const displayName = testDisplayName ?? _.sample(names)!; const credentials = await homeserver.registerUser("user", password, displayName); - console.log(`Registered test user @user:localhost with displayname ${displayName}`); + console.log(`Registered test user ${credentials.userId} with displayname ${displayName}`); await use({ ...credentials, From 757a22a82977d60221685de45d05bdc57fee459b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 15:35:27 +0000 Subject: [PATCH 37/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/editing/editing.spec.ts | 2 +- playwright/e2e/lazy-loading/lazy-loading.spec.ts | 3 +++ playwright/e2e/spaces/spaces.spec.ts | 2 ++ .../threads-activity-centre/threadsActivityCentre.spec.ts | 5 +++++ playwright/e2e/spotlight/spotlight.spec.ts | 2 +- playwright/services.ts | 3 ++- 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts index 6fc9c495989..934c4aa42ec 100644 --- a/playwright/e2e/editing/editing.spec.ts +++ b/playwright/e2e/editing/editing.spec.ts @@ -32,7 +32,7 @@ function mkPadding(n: number): IContent { } test.describe("Editing", () => { - test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3123"); + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488"); // Edit "Message" const editLastMessage = async (page: Page, edit: string) => { diff --git a/playwright/e2e/lazy-loading/lazy-loading.spec.ts b/playwright/e2e/lazy-loading/lazy-loading.spec.ts index 23ccaf80858..b0b73ba69cd 100644 --- a/playwright/e2e/lazy-loading/lazy-loading.spec.ts +++ b/playwright/e2e/lazy-loading/lazy-loading.spec.ts @@ -11,8 +11,11 @@ import type { Locator, Page } from "@playwright/test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; import { test, expect } from "../../element-web-test"; import { Credentials } from "../../plugins/homeserver"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Lazy Loading", () => { + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488"); + const charlies: Bot[] = []; test.use({ diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts index 14df58fa8e6..5acb3a672fe 100644 --- a/playwright/e2e/spaces/spaces.spec.ts +++ b/playwright/e2e/spaces/spaces.spec.ts @@ -10,6 +10,7 @@ import type { Locator, Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix"; import { ElementAppPage } from "../../pages/ElementAppPage"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; async function openSpaceCreateMenu(page: Page): Promise { await page.getByRole("button", { name: "Create a space" }).click(); @@ -50,6 +51,7 @@ function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state" } test.describe("Spaces", () => { + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488"); test.use({ displayName: "Sue", botCreateOpts: { displayName: "BotBob" }, diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index dcdcf1a21a4..6135e472783 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -8,8 +8,13 @@ import { expect, test } from "."; import { CommandOrControl } from "../../utils"; +import { isDendrite } from "../../../plugins/homeserver/dendrite"; test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); test.use({ displayName: "Alice", botCreateOpts: { displayName: "Other User" }, diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index 1b3b724ce04..444c88d0884 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -40,7 +40,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise { - test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3123"); + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488"); const bot1Name = "BotBob"; let bot1: Bot; diff --git a/playwright/services.ts b/playwright/services.ts index 1c34898a945..3ec17624d0c 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -111,8 +111,9 @@ export const test = base.extend({ homeserver: async ( { homeserverType, logger, network, _homeserver: homeserver, synapseConfigOptions, mas }, use, + testInfo, ) => { - test.skip( + testInfo.skip( !(homeserver instanceof SynapseContainer) && Object.keys(synapseConfigOptions).length > 0, `Test specifies Synapse config options so is unsupported with ${homeserverType}`, ); From 048563a0a445d1b5c383606a6328d79cf08f6711 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 17:22:48 +0000 Subject: [PATCH 38/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups.spec.ts | 2 -- playwright/e2e/crypto/decryption-failure-messages.spec.ts | 2 ++ playwright/e2e/oidc/oidc-native.spec.ts | 2 -- .../plugins/homeserver/synapse/consentHomeserver.ts | 8 +++++--- playwright/plugins/homeserver/synapse/emailHomeserver.ts | 7 ++++--- .../plugins/homeserver/synapse/legacyOAuthHomeserver.ts | 7 ++++--- playwright/plugins/homeserver/synapse/masHomeserver.ts | 8 +++++--- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index 93da85b9b80..ff792f9785e 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -27,7 +27,6 @@ async function expectBackupVersionToBe(page: Page, version: string) { // which is faster but leaves us without crypto set up. test.describe("Encryption state after registration", () => { test.use(masHomeserver); - test.skip(isDendrite, "does not yet support MAS"); test("Key backup is enabled by default", async ({ page, mailhogClient, app }) => { await page.goto("/#/login"); @@ -54,7 +53,6 @@ test.describe("Encryption state after registration", () => { test.describe("Key backup reset from elsewhere", () => { test.use(masHomeserver); - test.skip(isDendrite, "does not yet support MAS"); test("Key backup is disabled when reset from elsewhere", async ({ page, mailhogClient, request, homeserver }) => { const testUsername = "alice"; diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts index e1952bfec60..529251b223b 100644 --- a/playwright/e2e/crypto/decryption-failure-messages.spec.ts +++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -28,6 +28,8 @@ test.describe("Cryptography", function () { }); test.describe("decryption failure messages", () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); + test("should handle device-relative historical messages", async ({ homeserver, page, diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 60c5bbf025a..7c3d7f84ffe 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -9,12 +9,10 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test.ts"; import { registerAccountMas } from "."; import { ElementAppPage } from "../../pages/ElementAppPage.ts"; -import { isDendrite } from "../../plugins/homeserver/dendrite"; import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { test.use(masHomeserver); - test.skip(isDendrite, "does not yet support MAS"); test.slow(); // trace recording takes a while here test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => { diff --git a/playwright/plugins/homeserver/synapse/consentHomeserver.ts b/playwright/plugins/homeserver/synapse/consentHomeserver.ts index 07d1fbbec2c..348b6da4fe8 100644 --- a/playwright/plugins/homeserver/synapse/consentHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/consentHomeserver.ts @@ -8,10 +8,12 @@ Please see LICENSE files in the repository root for full details. import { Fixtures } from "@playwright/test"; -import { Services } from "../../../services.ts"; +import { Options, Services } from "../../../services.ts"; + +export const consentHomeserver: Fixtures = { + _homeserver: async ({ homeserverType, _homeserver: container, mailhog }, use, testInfo) => { + testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); -export const consentHomeserver: Fixtures = { - _homeserver: async ({ _homeserver: container, mailhog }, use) => { container .withCopyDirectoriesToContainer([ { source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" }, diff --git a/playwright/plugins/homeserver/synapse/emailHomeserver.ts b/playwright/plugins/homeserver/synapse/emailHomeserver.ts index ab7affdee52..c9a5daea7ac 100644 --- a/playwright/plugins/homeserver/synapse/emailHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/emailHomeserver.ts @@ -8,10 +8,11 @@ Please see LICENSE files in the repository root for full details. import { Fixtures } from "@playwright/test"; -import { Services } from "../../../services.ts"; +import { Options, Services } from "../../../services.ts"; -export const emailHomeserver: Fixtures = { - _homeserver: async ({ _homeserver: container, mailhog }, use) => { +export const emailHomeserver: Fixtures = { + _homeserver: async ({ homeserverType, _homeserver: container, mailhog }, use, testInfo) => { + testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); container.withConfig({ enable_registration_without_verification: undefined, disable_msisdn_registration: undefined, diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts index a6b39e04845..4dc165bb9b6 100644 --- a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -9,11 +9,12 @@ Please see LICENSE files in the repository root for full details. import { Fixtures } from "@playwright/test"; import { TestContainers } from "testcontainers"; -import { Services } from "../../../services.ts"; +import { Options, Services } from "../../../services.ts"; import { OAuthServer } from "../../oauth_server"; -export const legacyOAuthHomeserver: Fixtures = { - _homeserver: async ({ _homeserver: container }, use) => { +export const legacyOAuthHomeserver: Fixtures = { + _homeserver: async ({ homeserverType, _homeserver: container }, use, testInfo) => { + testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); const server = new OAuthServer(); const port = server.start(); diff --git a/playwright/plugins/homeserver/synapse/masHomeserver.ts b/playwright/plugins/homeserver/synapse/masHomeserver.ts index cb144fcaeeb..4a4c331571b 100644 --- a/playwright/plugins/homeserver/synapse/masHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/masHomeserver.ts @@ -8,13 +8,15 @@ Please see LICENSE files in the repository root for full details. import { Fixtures, PlaywrightTestArgs } from "@playwright/test"; -import { Services } from "../../../services.ts"; +import { Options, Services } from "../../../services.ts"; import { Fixtures as BaseFixtures } from "../../../element-web-test.ts"; import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts"; -type Fixture = PlaywrightTestArgs & Services & BaseFixtures; +type Fixture = PlaywrightTestArgs & Services & BaseFixtures & Options; export const masHomeserver: Fixtures = { - mas: async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => { + mas: async ({ homeserverType, _homeserver: homeserver, logger, network, postgres, mailhog }, use, testInfo) => { + testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); + const config = { clients: [ { From af8935e1595b0836cdad047827d7ce06b1a0b0c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jan 2025 18:00:31 +0000 Subject: [PATCH 39/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/room-directory/room-directory.spec.ts | 3 ++- playwright/e2e/sliding-sync/sliding-sync.spec.ts | 5 +++++ playwright/e2e/threads/threads.spec.ts | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/playwright/e2e/room-directory/room-directory.spec.ts b/playwright/e2e/room-directory/room-directory.spec.ts index 9006328ccec..b48a90b2b7d 100644 --- a/playwright/e2e/room-directory/room-directory.spec.ts +++ b/playwright/e2e/room-directory/room-directory.spec.ts @@ -10,6 +10,7 @@ import type { Preset, Visibility } from "matrix-js-sdk/src/matrix"; import { test, expect } from "../../element-web-test"; test.describe("Room Directory", () => { + test.skip(({ homeserverType }) => homeserverType === "pinecone", "Pinecone's /publicRooms API takes forever"); test.use({ displayName: "Ray", botCreateOpts: { displayName: "Paul" }, @@ -38,7 +39,7 @@ test.describe("Room Directory", () => { await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue(`#gaming:${user.homeServer}`); const checkbox = publishedAddresses .locator(".mx_SettingsFlag", { - hasText: "Publish this room to the public in localhost's room directory?", + hasText: `Publish this room to the public in ${user.homeServer}'s room directory?`, }) .getByRole("switch"); await checkbox.check(); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 35027746b0f..66a7318a040 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -69,6 +69,11 @@ const test = base.extend<{ }); test.describe("Sliding Sync", () => { + test.skip( + ({ homeserverType }) => homeserverType === "pinecone", + "due to a bug in Pinecone https://github.com/element-hq/dendrite/issues/3490", + ); + const checkOrder = async (wantOrder: string[], page: Page) => { await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder); }; diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index edcc0578d8a..6b8279e344b 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -8,8 +8,10 @@ Please see LICENSE files in the repository root for full details. import { SettingLevel } from "../../../src/settings/SettingLevel"; import { Layout } from "../../../src/settings/enums/Layout"; import { test, expect } from "../../element-web-test"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Threads", () => { + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3489"); test.use({ displayName: "Tom", botCreateOpts: { From cc871a66c03a22bd813f1d17c98d95152fa83a2d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 09:09:42 +0000 Subject: [PATCH 40/93] Fix playwright-image-updates.yaml workflow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/CODEOWNERS | 2 +- .github/workflows/playwright-image-updates.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index af52a6b77d1..695a94254e9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,5 +15,5 @@ /src/i18n/strings /src/i18n/strings/en_EN.json @element-hq/element-web-reviewers # Ignore the synapse plugin as this is updated by GHA for docker image updating -/playwright/plugins/homeserver/synapse/index.ts +/playwright/testcontainers/synapse.ts diff --git a/.github/workflows/playwright-image-updates.yaml b/.github/workflows/playwright-image-updates.yaml index ef2dfa11327..e5e2f739c09 100644 --- a/.github/workflows/playwright-image-updates.yaml +++ b/.github/workflows/playwright-image-updates.yaml @@ -17,7 +17,7 @@ jobs: docker pull "$IMAGE" INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE") DIGEST=${INSPECT#*@} - sed -i "s/const DOCKER_TAG.*/const DOCKER_TAG = \"develop@$DIGEST\";/" playwright/plugins/homeserver/synapse/index.ts + sed -i "s/const TAG.*/const TAG = \"develop@$DIGEST\";/" playwright/testcontainers/synapse.ts env: IMAGE: ghcr.io/element-hq/synapse:develop From 30c82d8714eda9639b57d710a4bb1e67304158ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 09:12:33 +0000 Subject: [PATCH 41/93] Add `X-Run-All-Tests` label for running all tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/labels.yml | 3 +++ .github/workflows/end-to-end-tests.yaml | 12 ++++++------ docs/playwright.md | 2 ++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index 80c5408c1e3..c8a34c47716 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -210,6 +210,9 @@ - name: "X-Upcoming-Release-Blocker" description: "This does not affect the current release cycle but will affect the next one" color: "e99695" +- name: "X-Run-All-Tests" + description: "When applied to PRs, it'll run the full gamut of end-to-end tests on the PR" + color: "ff7979" - name: "Z-Actions" color: "ededed" - name: "Z-Cache-Confusion" diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 6afabdb1fe6..4232993fa0b 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -114,13 +114,13 @@ jobs: - Chrome - Firefox - WebKit - isCron: - - ${{ github.event_name == 'schedule' }} - # Skip the Firefox & Safari runs unless this was a cron trigger + runAllTests: + - ${{ github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }} + # Skip the Firefox & Safari runs unless this was a cron trigger or PR has X-Run-All-Tests label exclude: - - isCron: false + - runAllTests: false project: Firefox - - isCron: false + - runAllTests: false project: WebKit steps: - uses: actions/checkout@v4 @@ -170,7 +170,7 @@ jobs: yarn playwright test \ --shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \ --project="${{ matrix.project }}" \ - ${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }} + ${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }} - name: Upload blob report to GitHub Actions Artifacts if: always() diff --git a/docs/playwright.md b/docs/playwright.md index fe44a06ff17..cd19c41b6cc 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -227,6 +227,8 @@ has to be disabled in Playwright on Firefox & Webkit to retain routing functiona Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available there at this time. +If you wish to run all tests in a PR, you can give it the label `X-Run-All-Tests`. + ## Supporter container runtimes We use testcontainers to spin up various instances of Synapse, Matrix Authentication Service, and more. From 0f5e135224e50dd07242e0a656d448c3da21a21b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 09:15:20 +0000 Subject: [PATCH 42/93] Ignore failing tests in stale-screenshot-reporter.ts to avoid confusing errors Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/stale-screenshot-reporter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright/stale-screenshot-reporter.ts b/playwright/stale-screenshot-reporter.ts index 3e38f78ca93..dc934827c1d 100644 --- a/playwright/stale-screenshot-reporter.ts +++ b/playwright/stale-screenshot-reporter.ts @@ -23,6 +23,7 @@ class StaleScreenshotReporter implements Reporter { private success = true; public onTestEnd(test: TestCase): void { + if (!test.ok()) return; for (const annotation of test.annotations) { if (annotation.type === "_screenshot") { this.screenshots.add(annotation.description); From 13527e735b467204ce3049c84ebc1af612906a86 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 11:02:05 +0000 Subject: [PATCH 43/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/one-to-one-chat/one-to-one-chat.spec.ts | 3 +++ src/TextForEvent.tsx | 5 +++-- src/components/views/messages/TextualEvent.tsx | 16 +--------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts index deefb305dbc..37b510faa5d 100644 --- a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts +++ b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts @@ -9,12 +9,15 @@ Please see LICENSE files in the repository root for full details. import { test as base, expect } from "../../element-web-test"; import { Credentials } from "../../plugins/homeserver"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const test = base.extend<{ user2?: Credentials; }>({}); test.describe("1:1 chat room", () => { + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3492"); + test.use({ displayName: "Jeff", user2: async ({ homeserver }, use) => { diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 1194e4ca716..bdb7e8cbe0e 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -107,8 +107,9 @@ function textForMemberEvent( allowJSX: boolean, showHiddenEvents?: boolean, ): (() => string) | null { - const senderName = getRoomMemberDisplayname(client, ev); - const targetName = getRoomMemberDisplayname(client, ev, ev.getStateKey()); + // XXX: SYJS-16 "sender is sometimes null for join messages" + const senderName = ev.sender?.name || getRoomMemberDisplayname(client, ev); + const targetName = ev.target?.name || getRoomMemberDisplayname(client, ev, ev.getStateKey()); const prevContent = ev.getPrevContent(); const content = ev.getContent(); const reason = content.reason; diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index 831aaee1175..8549fc5cab5 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixEvent, RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import RoomContext from "../../../contexts/RoomContext"; import * as TextForEvent from "../../../TextForEvent"; @@ -21,20 +21,6 @@ export default class TextualEvent extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; - public componentDidMount(): void { - MatrixClientPeg.get()?.on(RoomMemberEvent.Name, this.onMemberNameUpdate); - } - - public componentWillUnmount(): void { - MatrixClientPeg.get()?.off(RoomMemberEvent.Name, this.onMemberNameUpdate); - } - - private onMemberNameUpdate = (event: MatrixEvent, member: RoomMember): void => { - if (member.userId === this.props.mxEvent.getSender() || member.userId === this.props.mxEvent.getStateKey()) { - this.forceUpdate(); - } - }; - public render(): React.ReactNode { const text = TextForEvent.textForEvent( this.props.mxEvent, From 4a2a93e611e64d53b79d6b4476c966fd6842d87c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 11:07:20 +0000 Subject: [PATCH 44/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/end-to-end-tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index f6aad8af7e8..4292d3ccabe 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -111,9 +111,9 @@ jobs: # Run multiple instances in parallel to speed up the tests runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }} project: - - Chrome - - Firefox - - WebKit + # - Chrome + # - Firefox + # - WebKit - Dendrite - Pinecone runAllTests: From 91370c6d8f546a94366850e23c4750f21579efe0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 14:09:40 +0000 Subject: [PATCH 45/93] Specify Synapse ui_auth.session_timeout only on tests which require it As Dendrite lacks this configuration option Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/synapse.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 3b06db2325e..084ea480051 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -118,9 +118,7 @@ const DEFAULT_CONFIG = { password_config: { enabled: true, }, - ui_auth: { - session_timeout: "300s", - }, + ui_auth: {}, background_updates: { // Inhibit background updates as this Synapse isn't long-lived min_batch_size: 100000, From ca38b0ff78d9f791c786e33a2ed57c7bd0e1b44d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 14:25:48 +0000 Subject: [PATCH 46/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/settings/device-management.spec.ts | 3 +++ .../uiaLongSessionTimeoutHomeserver.ts | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts diff --git a/playwright/e2e/settings/device-management.spec.ts b/playwright/e2e/settings/device-management.spec.ts index 4c1169ad094..949ce582e3a 100644 --- a/playwright/e2e/settings/device-management.spec.ts +++ b/playwright/e2e/settings/device-management.spec.ts @@ -7,7 +7,10 @@ Please see LICENSE files in the repository root for full details. */ import { test, expect } from "../../element-web-test"; +import { uiaLongSessionTimeoutHomeserver } from "../../plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts"; +// This is needed to not get stopped by UIA when deleting other devices +test.use(uiaLongSessionTimeoutHomeserver); test.describe("Device manager", () => { test.use({ displayName: "Alice", diff --git a/playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts b/playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts new file mode 100644 index 00000000000..1a0e41a71f7 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts @@ -0,0 +1,24 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { Fixtures } from "@playwright/test"; + +import { Services } from "../../../services.ts"; + +export const uiaLongSessionTimeoutHomeserver: Fixtures<{}, Services> = { + synapseConfigOptions: [ + async ({ synapseConfigOptions }, use) => { + await use({ + ...synapseConfigOptions, + ui_auth: { + session_timeout: "300s", + }, + }); + }, + { scope: "worker" }, + ], +}; From f7170ea73ed458d015e50a1630c54f8bb9bc7d39 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 15:27:36 +0000 Subject: [PATCH 47/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts b/playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts index 1a0e41a71f7..1e389e2eac2 100644 --- a/playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts @@ -5,11 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { Fixtures } from "@playwright/test"; +import { Fixtures } from "../../../element-web-test.ts"; -import { Services } from "../../../services.ts"; - -export const uiaLongSessionTimeoutHomeserver: Fixtures<{}, Services> = { +export const uiaLongSessionTimeoutHomeserver: Fixtures = { synapseConfigOptions: [ async ({ synapseConfigOptions }, use) => { await use({ From c890dfcc72eb9deda716b894c75f2bae1f01a8e1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 15:52:55 +0000 Subject: [PATCH 48/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/read-receipts/editing-messages-in-threads.spec.ts | 6 ++++++ .../read-receipts/editing-messages-main-timeline.spec.ts | 6 ++++++ .../e2e/read-receipts/editing-messages-thread-roots.spec.ts | 6 ++++++ .../e2e/read-receipts/new-messages-in-threads.spec.ts | 6 ++++++ .../e2e/read-receipts/new-messages-main-timeline.spec.ts | 6 ++++++ .../e2e/read-receipts/new-messages-thread-roots.spec.ts | 6 ++++++ playwright/e2e/read-receipts/reactions-in-threads.spec.ts | 6 ++++++ .../e2e/read-receipts/reactions-main-timeline.spec.ts | 6 ++++++ playwright/e2e/read-receipts/redactions-in-threads.spec.ts | 6 ++++++ .../e2e/read-receipts/redactions-main-timeline.spec.ts | 6 ++++++ .../e2e/read-receipts/redactions-thread-roots.spec.ts | 6 ++++++ 11 files changed, 66 insertions(+) diff --git a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts index da27d39a2ca..092f1762edd 100644 --- a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("editing messages", () => { test.describe("in threads", () => { test("An edit of a threaded message makes the room unread", async ({ diff --git a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts index bc4eff711b0..1cb871230cd 100644 --- a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("editing messages", () => { test.describe("in the main timeline", () => { test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { diff --git a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts index a750fd9ba76..292f1137dc5 100644 --- a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("editing messages", () => { test.describe("thread roots", () => { test("An edit of a thread root leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts index 8deef2d2f52..89469fe9481 100644 --- a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { many, test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("new messages", () => { test.describe("in threads", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts index 4f94e7b09fb..3937b16426a 100644 --- a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { many, test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("new messages", () => { test.describe("in the main timeline", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts index 0e101d311a4..950e3f83ea3 100644 --- a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { many, test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("new messages", () => { test.describe("thread roots", () => { test("Reading a thread root does not mark the thread as read", async ({ diff --git a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts index eb733d3a12d..7af21e67991 100644 --- a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test, expect } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("reactions", () => { test.describe("in threads", () => { test("A reaction to a threaded message does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts index d8c16473836..1b577289357 100644 --- a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("reactions", () => { test.describe("in the main timeline", () => { test("Receiving a reaction to a message does not make a room unread", async ({ diff --git a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts index dc229d0b1b7..ab4e77471d4 100644 --- a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("redactions", () => { test.describe("in threads", () => { test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts index 356d03938f1..90cc6167361 100644 --- a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("redactions", () => { test.describe("in the main timeline", () => { test("Redacting the message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts index d875b0cecb3..691e9a39a86 100644 --- a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip( + isDendrite, + "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", + ); + test.describe("redactions", () => { test.describe("thread roots", () => { test("Redacting a thread root after it was read leaves the room read", async ({ From 9f5c32a8e8b9cfd102073ff7444f2dad6591c6ef Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 16:18:28 +0000 Subject: [PATCH 49/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/read-receipts/editing-messages-in-threads.spec.ts | 5 +---- .../e2e/read-receipts/editing-messages-main-timeline.spec.ts | 5 +---- .../e2e/read-receipts/editing-messages-thread-roots.spec.ts | 5 +---- playwright/e2e/read-receipts/high-level.spec.ts | 2 ++ playwright/e2e/read-receipts/new-messages-in-threads.spec.ts | 5 +---- .../e2e/read-receipts/new-messages-main-timeline.spec.ts | 5 +---- .../e2e/read-receipts/new-messages-thread-roots.spec.ts | 5 +---- playwright/e2e/read-receipts/reactions-in-threads.spec.ts | 5 +---- playwright/e2e/read-receipts/reactions-main-timeline.spec.ts | 5 +---- playwright/e2e/read-receipts/reactions-thread-roots.spec.ts | 2 ++ playwright/e2e/read-receipts/read-receipts.spec.ts | 2 ++ playwright/e2e/read-receipts/redactions-in-threads.spec.ts | 5 +---- .../e2e/read-receipts/redactions-main-timeline.spec.ts | 5 +---- playwright/e2e/read-receipts/redactions-thread-roots.spec.ts | 5 +---- 14 files changed, 17 insertions(+), 44 deletions(-) diff --git a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts index 092f1762edd..4fa204bacea 100644 --- a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts @@ -12,10 +12,7 @@ import { test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("editing messages", () => { test.describe("in threads", () => { diff --git a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts index 1cb871230cd..6c9596a5b2b 100644 --- a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts @@ -12,10 +12,7 @@ import { test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("editing messages", () => { test.describe("in the main timeline", () => { diff --git a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts index 292f1137dc5..9cd158430a8 100644 --- a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts @@ -12,10 +12,7 @@ import { test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("editing messages", () => { test.describe("thread roots", () => { diff --git a/playwright/e2e/read-receipts/high-level.spec.ts b/playwright/e2e/read-receipts/high-level.spec.ts index 627b2d348d3..a723928c57a 100644 --- a/playwright/e2e/read-receipts/high-level.spec.ts +++ b/playwright/e2e/read-receipts/high-level.spec.ts @@ -9,8 +9,10 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { customEvent, many, test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.slow(); test.describe("Ignored events", () => { diff --git a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts index 89469fe9481..2f3c153f201 100644 --- a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts @@ -12,10 +12,7 @@ import { many, test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("new messages", () => { test.describe("in threads", () => { diff --git a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts index 3937b16426a..16c8132378a 100644 --- a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts @@ -12,10 +12,7 @@ import { many, test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("new messages", () => { test.describe("in the main timeline", () => { diff --git a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts index 950e3f83ea3..a711d889a16 100644 --- a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts @@ -12,10 +12,7 @@ import { many, test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("new messages", () => { test.describe("thread roots", () => { diff --git a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts index 7af21e67991..b88e18afd8b 100644 --- a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts @@ -12,10 +12,7 @@ import { test, expect } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("reactions", () => { test.describe("in threads", () => { diff --git a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts index 1b577289357..77ed8cd5823 100644 --- a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts @@ -12,10 +12,7 @@ import { test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("reactions", () => { test.describe("in the main timeline", () => { diff --git a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts index d83d55e5dca..a6d21cb34ed 100644 --- a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts @@ -9,8 +9,10 @@ Please see LICENSE files in the repository root for full details. /* See readme.md for tips on writing these tests. */ import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("reactions", () => { test.describe("thread roots", () => { test("A reaction to a thread root does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/read-receipts.spec.ts b/playwright/e2e/read-receipts/read-receipts.spec.ts index 5d42513b560..8e079e14fdd 100644 --- a/playwright/e2e/read-receipts/read-receipts.spec.ts +++ b/playwright/e2e/read-receipts/read-receipts.spec.ts @@ -12,8 +12,10 @@ import { expect } from "../../element-web-test"; import { ElementAppPage } from "../../pages/ElementAppPage"; import { Bot } from "../../pages/bot"; import { test } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.use({ displayName: "Mae", botCreateOpts: { displayName: "Other User" }, diff --git a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts index ab4e77471d4..4e8b6bef5a3 100644 --- a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts @@ -12,10 +12,7 @@ import { test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("redactions", () => { test.describe("in threads", () => { diff --git a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts index 90cc6167361..203cbb997f8 100644 --- a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts @@ -12,10 +12,7 @@ import { test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("redactions", () => { test.describe("in the main timeline", () => { diff --git a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts index 691e9a39a86..108e61df34e 100644 --- a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts @@ -12,10 +12,7 @@ import { test } from "."; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Read receipts", { tag: "@mergequeue" }, () => { - test.skip( - isDendrite, - "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", - ); + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); test.describe("redactions", () => { test.describe("thread roots", () => { From bde3142b284a99005861ebcbc69672eb5640a3b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 17:37:15 +0000 Subject: [PATCH 50/93] React to MatrixEvent sender/target sentinels being updated for rendering state events Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/TextualEvent.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index 8549fc5cab5..41a8fdf1154 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; import RoomContext from "../../../contexts/RoomContext"; import * as TextForEvent from "../../../TextForEvent"; @@ -21,6 +21,19 @@ export default class TextualEvent extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; + public componentDidMount(): void { + this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + public componentWillUnmount(): void { + this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + + private onEventSentinelUpdated = (): void => { + // XXX: this is crap, but we don't have a better way to force a re-render + // Many TextForEvent handlers render parts of `event.sender` and `event.target` so ensure they are updated + this.forceUpdate(); + }; + public render(): React.ReactNode { const text = TextForEvent.textForEvent( this.props.mxEvent, From 5ca8d741176b32c9d1c9e55f799686ba3363b2f7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Jan 2025 17:46:28 +0000 Subject: [PATCH 51/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/ForwardDialog.tsx | 3 ++- src/components/views/elements/EventTilePreview.tsx | 3 ++- src/hooks/usePinnedEvents.ts | 2 +- src/indexing/EventIndex.ts | 3 ++- src/utils/exportUtils/Exporter.ts | 7 +------ src/utils/exportUtils/HtmlExport.tsx | 3 ++- test/test-utils/test-utils.ts | 6 ++++-- test/unit-tests/TextForEvent-test.ts | 3 ++- .../stores/room-list/previews/ReactionEventPreview-test.ts | 5 +++-- 9 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index a831acc5d18..08de3877069 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -237,7 +237,8 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr room_id: event.getRoomId(), origin_server_ts: event.getTs(), }); - mockEvent.sender = { + // @ts-ignore - private field access + mockEvent._sender = { name: profileInfo.displayname || userId, rawDisplayName: profileInfo.displayname, userId, diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index dc276858bd9..fd30dfe0ea7 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -89,7 +89,8 @@ export default class EventTilePreview extends React.Component { /* eslint-enable quote-props */ // Fake it more - event.sender = { + // @ts-ignore - private field access + event._sender = { name: this.props.displayName || this.props.userId, rawDisplayName: this.props.displayName, userId: this.props.userId, diff --git a/src/hooks/usePinnedEvents.ts b/src/hooks/usePinnedEvents.ts index bdda4a77013..b065edc83f7 100644 --- a/src/hooks/usePinnedEvents.ts +++ b/src/hooks/usePinnedEvents.ts @@ -154,7 +154,7 @@ async function fetchPinnedEvent(room: Room, pinnedEventId: string, cli: MatrixCl const senderUserId = event.getSender(); if (senderUserId && PinningUtils.isUnpinnable(event)) { // Inject sender information - event.sender = room.getMember(senderUserId); + event.setMetadata(room.currentState, false); // Also inject any edits we've found if (edit) event.makeReplaced(edit); diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index 7a126b2ffb6..18a2f9d26a9 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -761,7 +761,8 @@ export default class EventIndex extends EventEmitter { // We set this manually to avoid emitting RoomMember.membership and // RoomMember.name events. member.events.member = memberEvent; - matrixEvent.sender = member; + // @ts-ignore - private field access + matrixEvent._sender = member; return matrixEvent; }); diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts index 3852443d1e6..0c549038b47 100644 --- a/src/utils/exportUtils/Exporter.ts +++ b/src/utils/exportUtils/Exporter.ts @@ -110,12 +110,7 @@ export default abstract class Exporter { } protected setEventMetadata(event: MatrixEvent): MatrixEvent { - const roomState = this.room.currentState; - const sender = event.getSender(); - event.sender = (!!sender && roomState?.getSentinelMember(sender)) || null; - if (event.getType() === "m.room.member") { - event.target = roomState?.getSentinelMember(event.getStateKey()!) ?? null; - } + event.setMetadata(this.room.currentState, false); return event; } diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 9bd88dbd45e..bc153639bb0 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -350,7 +350,8 @@ export default class HTMLExporter extends Exporter { } const modifiedEvent = new MatrixEvent(); modifiedEvent.event = mxEv.event; - modifiedEvent.sender = mxEv.sender; + // @ts-ignore - private field access + modifiedEvent._sender = mxEv.sender; modifiedEvent.event.type = "m.room.message"; modifiedEvent.event.content = modifiedContent; return modifiedEvent; diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 1c78ec024c3..e0e3f630884 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -405,7 +405,8 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent { const mxEvent = opts.event ? new MatrixEvent(event) : (event as unknown as MatrixEvent); if (!mxEvent.sender && opts.user && opts.room) { - mxEvent.sender = { + // @ts-ignore - private field access + mxEvent._sender = { userId: opts.user, membership: KnownMembership.Join, name: opts.user, @@ -470,7 +471,8 @@ export function mkMembership( } const e = mkEvent(event); if (opts.target) { - e.target = opts.target; + // @ts-ignore - private field access + e._target = opts.target; } return e; } diff --git a/test/unit-tests/TextForEvent-test.ts b/test/unit-tests/TextForEvent-test.ts index 4dfccbb93e9..4dbf9badf71 100644 --- a/test/unit-tests/TextForEvent-test.ts +++ b/test/unit-tests/TextForEvent-test.ts @@ -179,7 +179,8 @@ describe("TextForEvent", () => { }, }, }); - mxEvent.sender = { name: userA.name } as RoomMember; + // @ts-ignore - private field access + mxEvent._sender = { name: userA.name } as RoomMember; return mxEvent; }; diff --git a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts index 8e01460abf2..31ecdf1e4f9 100644 --- a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts +++ b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts @@ -122,8 +122,9 @@ describe("ReactionEventPreview", () => { type: "m.reaction", room: roomId, }); - event.sender = new RoomMember(roomId, userId); - event.sender.name = "Bob"; + // @ts-ignore - private field access + event._sender = new RoomMember(roomId, userId); + event.sender!.name = "Bob"; expect(preview.getTextFor(event)).toMatchInlineSnapshot(`"Bob reacted 🪿 to duck duck goose"`); }); From 668da4844136a5e407463fffccc1f3235b4db93c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Jan 2025 10:12:16 +0000 Subject: [PATCH 52/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/device-verification.spec.ts | 7 ++++--- playwright/e2e/polls/polls.spec.ts | 3 +++ playwright/e2e/room_options/marked_unread.spec.ts | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index a028bfb70c2..d3fa2f33c99 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -219,9 +219,10 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { /* And we're all done! */ const infoDialog = page.locator(".mx_InfoDialog"); await infoDialog.getByRole("button", { name: "They match" }).click(); - await expect( - infoDialog.getByText(`You've successfully verified (${aliceBotClient.credentials.deviceId})!`), - ).toBeVisible(); + // We don't assert the full string as the device name is unset on Synapse but set to the user ID on Dendrite + await expect(infoDialog.getByText(`You've successfully verified`)).toContainText( + `(${aliceBotClient.credentials.deviceId})`, + ); await infoDialog.getByRole("button", { name: "Got it" }).click(); }); }); diff --git a/playwright/e2e/polls/polls.spec.ts b/playwright/e2e/polls/polls.spec.ts index 727c453a31c..fc49906b47c 100644 --- a/playwright/e2e/polls/polls.spec.ts +++ b/playwright/e2e/polls/polls.spec.ts @@ -11,8 +11,11 @@ import { Bot } from "../../pages/bot"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import { Layout } from "../../../src/settings/enums/Layout"; import type { Locator, Page } from "@playwright/test"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Polls", () => { + test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3492"); + type CreatePollOptions = { title: string; options: string[]; diff --git a/playwright/e2e/room_options/marked_unread.spec.ts b/playwright/e2e/room_options/marked_unread.spec.ts index b314152e684..2817bbc921a 100644 --- a/playwright/e2e/room_options/marked_unread.spec.ts +++ b/playwright/e2e/room_options/marked_unread.spec.ts @@ -7,10 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import { test, expect } from "../../element-web-test"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const TEST_ROOM_NAME = "The mark unread test room"; test.describe("Mark as Unread", () => { + test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970"); + test.use({ displayName: "Tom", botCreateOpts: { @@ -48,6 +51,6 @@ test.describe("Mark as Unread", () => { await roomTile.getByRole("button", { name: "Room options" }).click(); await page.getByRole("menuitem", { name: "Mark as unread" }).click(); - expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible(); + await expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible(); }); }); From 3d93ab89228f3ecf087ce751568393d3681c6b71 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Jan 2025 10:34:12 +0000 Subject: [PATCH 53/93] Remove test code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/end-to-end-tests.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 4292d3ccabe..bd7872ee120 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -111,13 +111,13 @@ jobs: # Run multiple instances in parallel to speed up the tests runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }} project: - # - Chrome - # - Firefox - # - WebKit + - Chrome + - Firefox + - WebKit - Dendrite - Pinecone runAllTests: - - true + - ${{ github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }} # Skip the Firefox & Safari runs unless this was a cron trigger or PR has X-Run-All-Tests label exclude: - runAllTests: false From 658527ce1343e2a05cb0c2abb4dfb35d03c43f97 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Jan 2025 10:39:39 +0000 Subject: [PATCH 54/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/ForwardDialog.tsx | 3 +-- src/components/views/elements/EventTilePreview.tsx | 3 +-- src/indexing/EventIndex.ts | 3 +-- src/utils/exportUtils/HtmlExport.tsx | 3 +-- test/test-utils/test-utils.ts | 3 +-- test/unit-tests/TextForEvent-test.ts | 3 +-- .../stores/room-list/previews/ReactionEventPreview-test.ts | 5 ++--- 7 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 08de3877069..a831acc5d18 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -237,8 +237,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr room_id: event.getRoomId(), origin_server_ts: event.getTs(), }); - // @ts-ignore - private field access - mockEvent._sender = { + mockEvent.sender = { name: profileInfo.displayname || userId, rawDisplayName: profileInfo.displayname, userId, diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index fd30dfe0ea7..dc276858bd9 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -89,8 +89,7 @@ export default class EventTilePreview extends React.Component { /* eslint-enable quote-props */ // Fake it more - // @ts-ignore - private field access - event._sender = { + event.sender = { name: this.props.displayName || this.props.userId, rawDisplayName: this.props.displayName, userId: this.props.userId, diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index 18a2f9d26a9..7a126b2ffb6 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -761,8 +761,7 @@ export default class EventIndex extends EventEmitter { // We set this manually to avoid emitting RoomMember.membership and // RoomMember.name events. member.events.member = memberEvent; - // @ts-ignore - private field access - matrixEvent._sender = member; + matrixEvent.sender = member; return matrixEvent; }); diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index bc153639bb0..9bd88dbd45e 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -350,8 +350,7 @@ export default class HTMLExporter extends Exporter { } const modifiedEvent = new MatrixEvent(); modifiedEvent.event = mxEv.event; - // @ts-ignore - private field access - modifiedEvent._sender = mxEv.sender; + modifiedEvent.sender = mxEv.sender; modifiedEvent.event.type = "m.room.message"; modifiedEvent.event.content = modifiedContent; return modifiedEvent; diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index e0e3f630884..20479e045ad 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -405,8 +405,7 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent { const mxEvent = opts.event ? new MatrixEvent(event) : (event as unknown as MatrixEvent); if (!mxEvent.sender && opts.user && opts.room) { - // @ts-ignore - private field access - mxEvent._sender = { + mxEvent.sender = { userId: opts.user, membership: KnownMembership.Join, name: opts.user, diff --git a/test/unit-tests/TextForEvent-test.ts b/test/unit-tests/TextForEvent-test.ts index 4dbf9badf71..4dfccbb93e9 100644 --- a/test/unit-tests/TextForEvent-test.ts +++ b/test/unit-tests/TextForEvent-test.ts @@ -179,8 +179,7 @@ describe("TextForEvent", () => { }, }, }); - // @ts-ignore - private field access - mxEvent._sender = { name: userA.name } as RoomMember; + mxEvent.sender = { name: userA.name } as RoomMember; return mxEvent; }; diff --git a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts index 31ecdf1e4f9..8e01460abf2 100644 --- a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts +++ b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts @@ -122,9 +122,8 @@ describe("ReactionEventPreview", () => { type: "m.reaction", room: roomId, }); - // @ts-ignore - private field access - event._sender = new RoomMember(roomId, userId); - event.sender!.name = "Bob"; + event.sender = new RoomMember(roomId, userId); + event.sender.name = "Bob"; expect(preview.getTextFor(event)).toMatchInlineSnapshot(`"Bob reacted 🪿 to duck duck goose"`); }); From f3bbfaea668edbc30a5a50e156831b31dc83b2b1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Jan 2025 10:39:59 +0000 Subject: [PATCH 55/93] React to sentinel changes in EventListSummary Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/elements/EventListSummary.tsx | 181 ++++++++++++------ 1 file changed, 125 insertions(+), 56 deletions(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index c052a83f661..e73ab3a85b9 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -9,8 +9,9 @@ Please see LICENSE files in the repository root for full details. */ import React, { ComponentProps, ReactNode } from "react"; -import { MatrixEvent, RoomMember, EventType } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, MatrixEventEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; +import { throttle } from "lodash"; import { _t } from "../../../languageHandler"; import { formatList } from "../../../utils/FormattingUtils"; @@ -22,6 +23,8 @@ import { Layout } from "../../../settings/enums/Layout"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import AccessibleButton from "./AccessibleButton"; import RoomContext from "../../../contexts/RoomContext"; +import { arrayHasDiff } from "../../../utils/arrays.ts"; +import { objectHasDiff } from "../../../utils/objects.ts"; const onPinnedMessagesClick = (): void => { RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); @@ -69,9 +72,14 @@ enum TransitionType { const SEP = ","; -export default class EventListSummary extends React.Component< - IProps & Required> -> { +type Props = IProps & Required>; + +interface State { + userEvents: Record; + summaryMembers: RoomMember[]; +} + +export default class EventListSummary extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; @@ -82,15 +90,123 @@ export default class EventListSummary extends React.Component< layout: Layout.Group, }; - public shouldComponentUpdate(nextProps: IProps): boolean { + public constructor(props: Props) { + super(props); + + this.state = this.generateState(); + } + + private generateState(): State { + const eventsToRender = this.props.events; + + // Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created, + // so this works perfectly for us to match event order whilst storing the latest Avatar Member + const latestUserAvatarMember = new Map(); + + // Object mapping user IDs to an array of IUserEvents + const userEvents: Record = {}; + eventsToRender.forEach((e, index) => { + const type = e.getType(); + + let userKey = e.getSender()!; + if (e.isState() && type === EventType.RoomThirdPartyInvite) { + userKey = e.getContent().display_name; + } else if (e.isState() && type === EventType.RoomMember) { + userKey = e.getStateKey()!; + } else if (e.isRedacted() && e.getUnsigned()?.redacted_because) { + userKey = e.getUnsigned().redacted_because!.sender; + } + + // Initialise a user's events + if (!userEvents[userKey]) { + userEvents[userKey] = []; + } + + let displayName = userKey; + if (e.isRedacted()) { + const sender = this.context?.room?.getMember(userKey); + if (sender) { + displayName = sender.name; + latestUserAvatarMember.set(userKey, sender); + } + } else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { + displayName = e.target.name; + latestUserAvatarMember.set(userKey, e.target); + } else if (e.sender && type !== EventType.RoomThirdPartyInvite) { + displayName = e.sender.name; + latestUserAvatarMember.set(userKey, e.sender); + } + + userEvents[userKey].push({ + mxEvent: e, + displayName, + index: index, + }); + }); + + return { + userEvents, + summaryMembers: Array.from(latestUserAvatarMember.values()), + }; + } + + public componentDidMount(): void { + this.bindSentinelListeners(this.props.events); + } + + public componentDidUpdate(prevProps: Readonly): void { + if (prevProps.events !== this.props.events) { + this.unbindSentinelListeners(prevProps.events); + this.bindSentinelListeners(this.props.events); + this.setState(this.generateState()); + } + } + + public componentWillUnmount(): void { + this.unbindSentinelListeners(this.props.events); + } + + private bindSentinelListeners(events: MatrixEvent[]): void { + for (const event of events) { + event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + } + + private unbindSentinelListeners(events: MatrixEvent[]): void { + for (const event of events) { + event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + } + + private onEventSentinelUpdated = throttle( + (): void => { + console.log("@@ SENTINEL UPDATED"); + this.setState(this.generateState()); + }, + 500, + { leading: true, trailing: true }, + ); + + public shouldComponentUpdate(nextProps: Props, nextState: State): boolean { // Update if // - The number of summarised events has changed // - or if the summary is about to toggle to become collapsed // - or if there are fewEvents, meaning the child eventTiles are shown as-is + // - or if the summary members have changed + // - or if the userEvents have changed return ( nextProps.events.length !== this.props.events.length || nextProps.events.length < this.props.threshold || - nextProps.layout !== this.props.layout + nextProps.layout !== this.props.layout || + arrayHasDiff(nextState.summaryMembers, this.state.summaryMembers) || + arrayHasDiff(Object.values(nextState.userEvents), Object.values(this.state.userEvents)) || + Object.keys(nextState.userEvents).length !== Object.keys(this.state.userEvents).length || + Object.keys(nextState.userEvents).some((userId) => + nextState.userEvents[userId].some((event, i) => + objectHasDiff(event, this.state.userEvents[userId]?.[i] ?? {}), + ), + ) || + true ); } @@ -492,54 +608,7 @@ export default class EventListSummary extends React.Component< } public render(): React.ReactNode { - const eventsToRender = this.props.events; - - // Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created, - // so this works perfectly for us to match event order whilst storing the latest Avatar Member - const latestUserAvatarMember = new Map(); - - // Object mapping user IDs to an array of IUserEvents - const userEvents: Record = {}; - eventsToRender.forEach((e, index) => { - const type = e.getType(); - - let userKey = e.getSender()!; - if (e.isState() && type === EventType.RoomThirdPartyInvite) { - userKey = e.getContent().display_name; - } else if (e.isState() && type === EventType.RoomMember) { - userKey = e.getStateKey()!; - } else if (e.isRedacted() && e.getUnsigned()?.redacted_because) { - userKey = e.getUnsigned().redacted_because!.sender; - } - - // Initialise a user's events - if (!userEvents[userKey]) { - userEvents[userKey] = []; - } - - let displayName = userKey; - if (e.isRedacted()) { - const sender = this.context?.room?.getMember(userKey); - if (sender) { - displayName = sender.name; - latestUserAvatarMember.set(userKey, sender); - } - } else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { - displayName = e.target.name; - latestUserAvatarMember.set(userKey, e.target); - } else if (e.sender && type !== EventType.RoomThirdPartyInvite) { - displayName = e.sender.name; - latestUserAvatarMember.set(userKey, e.sender); - } - - userEvents[userKey].push({ - mxEvent: e, - displayName, - index: index, - }); - }); - - const aggregate = this.getAggregate(userEvents); + const aggregate = this.getAggregate(this.state.userEvents); // Sort types by order of lowest event index within sequence const orderedTransitionSequences = Object.keys(aggregate.names).sort( @@ -552,9 +621,9 @@ export default class EventListSummary extends React.Component< events={this.props.events} threshold={this.props.threshold} onToggle={this.props.onToggle} - startExpanded={this.props.startExpanded} + startExpanded={true} children={this.props.children} - summaryMembers={[...latestUserAvatarMember.values()]} + summaryMembers={this.state.summaryMembers} layout={this.props.layout} summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} /> From 3051a9589194f18bfe6459397e8307a769f09ab1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Jan 2025 10:58:21 +0000 Subject: [PATCH 56/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/EventListSummary.tsx | 2 +- .../__snapshots__/PinnedMessagesCard-test.tsx.snap | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index e73ab3a85b9..157eb26a2d6 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -621,7 +621,7 @@ export default class EventListSummary extends React.Component { events={this.props.events} threshold={this.props.threshold} onToggle={this.props.onToggle} - startExpanded={true} + startExpanded={this.props.startExpanded} children={this.props.children} summaryMembers={this.state.summaryMembers} layout={this.props.layout} diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap index 1ffef77d9c0..4a4ac6d6d63 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap @@ -145,6 +145,7 @@ exports[` should show two pinned messages 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a @@ -222,6 +223,7 @@ exports[` should show two pinned messages 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a @@ -364,6 +366,7 @@ exports[` unpin all should not allow to unpinall 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a @@ -441,6 +444,7 @@ exports[` unpin all should not allow to unpinall 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a From e8910d96f5b49c21a35009727145e4ba0835b29d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Jan 2025 12:21:55 +0000 Subject: [PATCH 57/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/EventListSummary.tsx | 5 ++--- test/test-utils/test-utils.ts | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 157eb26a2d6..79d89aed0d0 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -193,7 +193,7 @@ export default class EventListSummary extends React.Component { // - or if the summary is about to toggle to become collapsed // - or if there are fewEvents, meaning the child eventTiles are shown as-is // - or if the summary members have changed - // - or if the userEvents have changed + // - or if the one of IUserEvents within userEvents have changed return ( nextProps.events.length !== this.props.events.length || nextProps.events.length < this.props.threshold || @@ -205,8 +205,7 @@ export default class EventListSummary extends React.Component { nextState.userEvents[userId].some((event, i) => objectHasDiff(event, this.state.userEvents[userId]?.[i] ?? {}), ), - ) || - true + ) ); } diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 20479e045ad..1c78ec024c3 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -470,8 +470,7 @@ export function mkMembership( } const e = mkEvent(event); if (opts.target) { - // @ts-ignore - private field access - e._target = opts.target; + e.target = opts.target; } return e; } From 45607bb72f79181d8fdddf9b5fc5de1182a4dfdf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 11:36:20 +0000 Subject: [PATCH 58/93] Docs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- docs/playwright.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/playwright.md b/docs/playwright.md index 315033955b2..a4c7d1b1886 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -77,6 +77,9 @@ test.use({ ``` The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration. +Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as +they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId. +We remove public rooms from the directory between tests but deleting users doesn't have a homeserver agnostic solution. The logs from testcontainers will be attached to any reports output from Playwright. ## Writing Tests From 1b8f62abe2487be07017c3cb05c4cc36161a13c2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 11:37:11 +0000 Subject: [PATCH 59/93] Avoid reusing user1234 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../forgot-password/forgot-password.spec.ts | 53 ++++++++++++------- .../one-to-one-chat/one-to-one-chat.spec.ts | 4 +- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index 71475e892eb..af4e6def7ed 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -6,16 +6,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { expect, test } from "../../element-web-test"; +import { expect, test as base } from "../../element-web-test"; import { selectHomeserver } from "../utils"; import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { Credentials } from "../../plugins/homeserver"; -const username = "user1234"; -// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. -const password = "oETo7MPf0o"; const email = "user@nowhere.dummy"; +const test = base.extend<{ credentials: Pick }>({ + // eslint-disable-next-line no-empty-pattern + credentials: async ({}, use, testInfo) => { + await use({ + username: `user_${testInfo.testId}`, + // this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. + password: "oETo7MPf0o", + }); + }, +}); + test.use(emailHomeserver); test.use({ config: { @@ -45,31 +54,35 @@ test.describe("Forgot Password", () => { await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); }); - test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { - const user = await homeserver.registerUser(username, password); + test( + "renders email verification dialog properly", + { tag: "@screenshot" }, + async ({ page, homeserver, credentials }) => { + const user = await homeserver.registerUser(credentials.username, credentials.password); - await homeserver.setThreepid(user.userId, "email", email); + await homeserver.setThreepid(user.userId, "email", email); - await page.goto("/"); + await page.goto("/"); - await page.getByRole("link", { name: "Sign in" }).click(); - await selectHomeserver(page, homeserver.baseUrl); + await page.getByRole("link", { name: "Sign in" }).click(); + await selectHomeserver(page, homeserver.baseUrl); - await page.getByRole("button", { name: "Forgot password?" }).click(); + await page.getByRole("button", { name: "Forgot password?" }).click(); - await page.getByRole("textbox", { name: "Email address" }).fill(email); + await page.getByRole("textbox", { name: "Email address" }).fill(email); - await page.getByRole("button", { name: "Send email" }).click(); + await page.getByRole("button", { name: "Send email" }).click(); - await page.getByRole("button", { name: "Next" }).click(); + await page.getByRole("button", { name: "Next" }).click(); - await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password); - await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password); + await page.getByRole("textbox", { name: "New Password", exact: true }).fill(credentials.password); + await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(credentials.password); - await page.getByRole("button", { name: "Reset password" }).click(); + await page.getByRole("button", { name: "Reset password" }).click(); - await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport(); + await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport(); - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png"); - }); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png"); + }, + ); }); diff --git a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts index deefb305dbc..8a4401f5f23 100644 --- a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts +++ b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts @@ -17,8 +17,8 @@ const test = base.extend<{ test.describe("1:1 chat room", () => { test.use({ displayName: "Jeff", - user2: async ({ homeserver }, use) => { - const credentials = await homeserver.registerUser("user1234", "p4s5W0rD", "Timmy"); + user2: async ({ homeserver }, use, testInfo) => { + const credentials = await homeserver.registerUser(`user2_${testInfo.testId}`, "p4s5W0rD", "Timmy"); await use(credentials); }, }); From b203e4b2a546d3edd3d7aab0ee4bf5947c915c88 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 11:37:20 +0000 Subject: [PATCH 60/93] Fix stale-screenshot-reporter.ts Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/stale-screenshot-reporter.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/playwright/stale-screenshot-reporter.ts b/playwright/stale-screenshot-reporter.ts index dc934827c1d..5c0e42ca2b2 100644 --- a/playwright/stale-screenshot-reporter.ts +++ b/playwright/stale-screenshot-reporter.ts @@ -20,10 +20,13 @@ const snapshotRoot = path.join(__dirname, "snapshots"); class StaleScreenshotReporter implements Reporter { private screenshots = new Set(); + private failing = false; private success = true; public onTestEnd(test: TestCase): void { - if (!test.ok()) return; + if (!test.ok()) { + this.failing = true; + } for (const annotation of test.annotations) { if (annotation.type === "_screenshot") { this.screenshots.add(annotation.description); @@ -40,6 +43,8 @@ class StaleScreenshotReporter implements Reporter { } public async onExit(): Promise { + if (this.failing) return; + const screenshotFiles = new Set(await glob(`**/*.png`, { cwd: snapshotRoot })); for (const screenshot of screenshotFiles) { if (screenshot.split("-").at(-1) !== "linux.png") { From 3eeb2216f9a79c124131b0631bf4920282167103 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 11:37:49 +0000 Subject: [PATCH 61/93] Clean up public rooms between tests on reused homeserver Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/services.ts | 1 + .../testcontainers/HomeserverContainer.ts | 6 +- playwright/testcontainers/dendrite.ts | 8 +- playwright/testcontainers/synapse.ts | 133 +++++++++++++----- 4 files changed, 104 insertions(+), 44 deletions(-) diff --git a/playwright/services.ts b/playwright/services.ts index b480cbc4054..1e2501dc2de 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -136,5 +136,6 @@ export const test = base.extend<{}, Services>({ await logger.testStarted(testInfo); await use(context); await logger.testFinished(testInfo); + await homeserver.onTestFinished(testInfo); }, }); diff --git a/playwright/testcontainers/HomeserverContainer.ts b/playwright/testcontainers/HomeserverContainer.ts index 09eea7da77a..73c9882418c 100644 --- a/playwright/testcontainers/HomeserverContainer.ts +++ b/playwright/testcontainers/HomeserverContainer.ts @@ -6,17 +6,17 @@ Please see LICENSE files in the repository root for full details. */ import { AbstractStartedContainer, GenericContainer } from "testcontainers"; -import { APIRequestContext } from "@playwright/test"; +import { APIRequestContext, TestInfo } from "@playwright/test"; -import { StartedSynapseContainer } from "./synapse.ts"; import { HomeserverInstance } from "../plugins/homeserver"; export interface HomeserverContainer extends GenericContainer { withConfigField(key: string, value: any): this; withConfig(config: Partial): this; - start(): Promise; + start(): Promise; } export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance { setRequest(request: APIRequestContext): void; + onTestFinished(testInfo: TestInfo): Promise; } diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index 629ea70c65c..517b0f7a642 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -235,7 +235,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon return this; } - public override async start(): Promise { + public override async start(): Promise { this.withCopyContentToContainer([ { target: "/etc/dendrite/dendrite.yaml", @@ -244,8 +244,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon ]); const container = await super.start(); - // Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it - return new StartedSynapseContainer( + return new StartedDendriteContainer( container, `http://${container.getHost()}:${container.getMappedPort(8008)}`, this.config.client_api.registration_shared_secret, @@ -258,3 +257,6 @@ export class PineconeContainer extends DendriteContainer { super("matrixdotorg/dendrite-demo-pinecone:main", "/usr/bin/dendrite-demo-pinecone"); } } + +// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it +export class StartedDendriteContainer extends StartedSynapseContainer {} diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 5111a6f0a66..27249b75952 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; -import { APIRequestContext } from "@playwright/test"; +import { APIRequestContext, TestInfo } from "@playwright/test"; import crypto from "node:crypto"; import * as YAML from "yaml"; import { set } from "lodash"; @@ -138,6 +138,8 @@ const DEFAULT_CONFIG = { }, }; +type Verb = "GET" | "POST" | "PUT" | "DELETE"; + export type SynapseConfigOptions = Partial; export class SynapseContainer extends GenericContainer implements HomeserverContainer { @@ -228,8 +230,8 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont } export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { - private adminToken?: string; - private request?: APIRequestContext; + private adminTokenPromise?: Promise; + protected _request?: APIRequestContext; constructor( container: StartedTestContainer, @@ -240,7 +242,24 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements } public setRequest(request: APIRequestContext): void { - this.request = request; + this._request = request; + } + + public async onTestFinished(testInfo: TestInfo): Promise { + // Clean up the server to prevent rooms leaking between tests + await this.deletePublicRooms(); + } + + protected async deletePublicRooms(): Promise { + // We hide the rooms from the room directory to save time between tests and for portability between homeservers + const { chunk: rooms } = await this.request<{ + chunk: { room_id: string }[]; + }>("GET", "v3/publicRooms", {}); + await Promise.all( + rooms.map((room) => + this.request("PUT", `v3/directory/list/room/${room.room_id}`, { visibility: "private" }), + ), + ); } private async registerUserInternal( @@ -250,12 +269,12 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements admin = false, ): Promise { const url = `${this.baseUrl}/_synapse/admin/v1/register`; - const { nonce } = await this.request.get(url).then((r) => r.json()); + const { nonce } = await this._request.get(url).then((r) => r.json()); const mac = crypto .createHmac("sha1", this.registrationSharedSecret) .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) .digest("hex"); - const res = await this.request.post(url, { + const res = await this._request.post(url, { data: { nonce, username, @@ -282,23 +301,76 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements }; } + protected async getAdminToken(): Promise { + if (this.adminTokenPromise === undefined) { + this.adminTokenPromise = this.registerUserInternal( + "admin", + "totalyinsecureadminpassword", + undefined, + true, + ).then((res) => res.accessToken); + } + return this.adminTokenPromise; + } + + private async adminRequest(verb: "GET", path: string, data?: never): Promise; + private async adminRequest(verb: Verb, path: string, data?: object): Promise; + private async adminRequest(verb: Verb, path: string, data?: object): Promise { + const adminToken = await this.getAdminToken(); + const url = `${this.baseUrl}/_synapse/admin/${path}`; + const res = await this._request.fetch(url, { + data, + method: verb, + headers: { + Authorization: `Bearer ${adminToken}`, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + + return res.json(); + } + + public async request(verb: "GET", path: string, data?: never): Promise; + public async request(verb: Verb, path: string, data?: object): Promise; + public async request(verb: Verb, path: string, data?: object): Promise { + const token = await this.getAdminToken(); + const url = `${this.baseUrl}/_matrix/client/${path}`; + const res = await this._request.fetch(url, { + data, + method: verb, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + + return res.json(); + } + public registerUser(username: string, password: string, displayName?: string): Promise { return this.registerUserInternal(username, password, displayName, false); } public async loginUser(userId: string, password: string): Promise { - const url = `${this.baseUrl}/_matrix/client/v3/login`; - const res = await this.request.post(url, { - data: { - type: "m.login.password", - identifier: { - type: "m.id.user", - user: userId, - }, - password: password, + const json = await this.request<{ + access_token: string; + user_id: string; + device_id: string; + home_server: string; + }>("POST", "v3/login", { + type: "m.login.password", + identifier: { + type: "m.id.user", + user: userId, }, + password: password, }); - const json = await res.json(); return { password, @@ -311,28 +383,13 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements } public async setThreepid(userId: string, medium: string, address: string): Promise { - if (this.adminToken === undefined) { - const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true); - this.adminToken = result.accessToken; - } - - const url = `${this.baseUrl}/_synapse/admin/v2/users/${userId}`; - const res = await this.request.put(url, { - data: { - threepids: [ - { - medium, - address, - }, - ], - }, - headers: { - Authorization: `Bearer ${this.adminToken}`, - }, + await this.adminRequest("PUT", `v2/users/${userId}`, { + threepids: [ + { + medium, + address, + }, + ], }); - - if (!res.ok()) { - throw await res.json(); - } } } From b7b650e0fc581aabdfb38721ff4ad8f09b315475 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 11:38:12 +0000 Subject: [PATCH 62/93] Deflake spotlight when homeserver is reused Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/spotlight/spotlight.spec.ts | 195 +++++++++++---------- 1 file changed, 100 insertions(+), 95 deletions(-) diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index d1bb3dec258..da35ca57b35 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import type { AccountDataEvents } from "matrix-js-sdk/src/matrix"; -import { test, expect } from "../../element-web-test"; +import type { AccountDataEvents, Visibility } from "matrix-js-sdk/src/matrix"; +import { test as base, expect } from "../../element-web-test"; import { Filter } from "../../pages/Spotlight"; import { Bot } from "../../pages/bot"; import type { Locator, Page } from "@playwright/test"; @@ -38,41 +38,37 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise { - const bot1Name = "BotBob"; - let bot1: Bot; - - const bot2Name = "ByteBot"; - let bot2: Bot; - - const room1Name = "247"; - let room1Id: string; - - const room2Name = "Lounge"; - let room2Id: string; - - const room3Name = "Public"; - let room3Id: string; - - test.use({ - displayName: "Jim", - }); - - test.beforeEach(async ({ page, homeserver, app, user }) => { - bot1 = new Bot(page, homeserver, { displayName: bot1Name, autoAcceptInvites: true }); - bot2 = new Bot(page, homeserver, { displayName: bot2Name, autoAcceptInvites: true }); - const Visibility = await page.evaluate(() => (window as any).matrixcs.Visibility); - - room1Id = await app.client.createRoom({ name: room1Name, visibility: Visibility.Public }); - - await bot1.joinRoom(room1Id); - const bot1UserId = await bot1.evaluate((client) => client.getUserId()); - room2Id = await bot2.createRoom({ name: room2Name, visibility: Visibility.Public }); - await bot2.inviteUser(room2Id, bot1UserId); - - room3Id = await bot2.createRoom({ - name: room3Name, - visibility: Visibility.Public, +type RoomRef = { name: string; roomId: string }; +const test = base.extend<{ + bot1: Bot; + bot2: Bot; + room1: RoomRef; + room2: RoomRef; + room3: RoomRef; +}>({ + bot1: async ({ page, homeserver }, use, testInfo) => { + const bot = new Bot(page, homeserver, { displayName: `BotBob_${testInfo.testId}`, autoAcceptInvites: true }); + await use(bot); + }, + bot2: async ({ page, homeserver }, use, testInfo) => { + const bot = new Bot(page, homeserver, { displayName: `ByteBot_${testInfo.testId}`, autoAcceptInvites: true }); + await use(bot); + }, + room1: async ({ app }, use) => { + const name = "247"; + const roomId = await app.client.createRoom({ name, visibility: "public" as Visibility }); + await use({ name, roomId }); + }, + room2: async ({ bot2 }, use) => { + const name = "Lounge"; + const roomId = await bot2.createRoom({ name, visibility: "public" as Visibility }); + await use({ name, roomId }); + }, + room3: async ({ bot2 }, use) => { + const name = "Public"; + const roomId = await bot2.createRoom({ + name, + visibility: "public" as Visibility, initial_state: [ { type: "m.room.history_visibility", @@ -83,9 +79,21 @@ test.describe("Spotlight", () => { }, ], }); - await bot2.inviteUser(room3Id, bot1UserId); + await use({ name, roomId }); + }, +}); + +test.describe("Spotlight", () => { + test.use({ + displayName: "Jim", + }); + + test.beforeEach(async ({ page, user, bot1, bot2, room1, room2, room3 }) => { + await bot1.joinRoom(room1.roomId); + await bot2.inviteUser(room2.roomId, bot1.credentials.userId); + await bot2.inviteUser(room3.roomId, bot1.credentials.userId); - await page.goto("/#/room/" + room1Id); + await page.goto(`/#/room/${room1.roomId}`); await expect(page.locator(".mx_RoomSublist_skeletonUI")).not.toBeAttached(); }); @@ -117,69 +125,69 @@ test.describe("Spotlight", () => { await expect(spotlight.dialog.locator(".mx_SpotlightDialog_filter")).not.toBeAttached(); }); - test("should find joined rooms", async ({ page, app }) => { + test("should find joined rooms", async ({ page, app, room1 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle - await spotlight.search(room1Name); + await spotlight.search(room1.name); const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(room1Name); + await expect(resultLocator.first()).toContainText(room1.name); await resultLocator.first().click(); - await expect(page).toHaveURL(new RegExp(`#/room/${room1Id}`)); - await expect(roomHeaderName(page)).toContainText(room1Name); + await expect(page).toHaveURL(new RegExp(`#/room/${room1.roomId}`)); + await expect(roomHeaderName(page)).toContainText(room1.name); }); - test("should find known public rooms", async ({ page, app }) => { + test("should find known public rooms", async ({ page, app, room1 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.PublicRooms); - await spotlight.search(room1Name); + await spotlight.search(room1.name); const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(room1Name); + await expect(resultLocator.first()).toContainText(room1.name); await expect(resultLocator.first()).toContainText("View"); await resultLocator.first().click(); - await expect(page).toHaveURL(new RegExp(`#/room/${room1Id}`)); - await expect(roomHeaderName(page)).toContainText(room1Name); + await expect(page).toHaveURL(new RegExp(`#/room/${room1.roomId}`)); + await expect(roomHeaderName(page)).toContainText(room1.name); }); - test("should find unknown public rooms", async ({ page, app }) => { + test("should find unknown public rooms", async ({ page, app, room2 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.PublicRooms); - await spotlight.search(room2Name); + await spotlight.search(room2.name); const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(room2Name); + await expect(resultLocator.first()).toContainText(room2.name); await expect(resultLocator.first()).toContainText("Join"); await resultLocator.first().click(); - await expect(page).toHaveURL(new RegExp(`#/room/${room2Id}`)); + await expect(page).toHaveURL(new RegExp(`#/room/${room2.roomId}`)); await expect(page.locator(".mx_RoomView_MessageList")).toHaveCount(1); - await expect(roomHeaderName(page)).toContainText(room2Name); + await expect(roomHeaderName(page)).toContainText(room2.name); }); - test("should find unknown public world readable rooms", async ({ page, app }) => { + test("should find unknown public world readable rooms", async ({ page, app, room3 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.PublicRooms); - await spotlight.search(room3Name); + await spotlight.search(room3.name); const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(room3Name); + await expect(resultLocator.first()).toContainText(room3.name); await expect(resultLocator.first()).toContainText("View"); await resultLocator.first().click(); - await expect(page).toHaveURL(new RegExp(`#/room/${room3Id}`)); + await expect(page).toHaveURL(new RegExp(`#/room/${room3.roomId}`)); await page.getByRole("button", { name: "Join the discussion" }).click(); - await expect(roomHeaderName(page)).toHaveText(room3Name); + await expect(roomHeaderName(page)).toHaveText(room3.name); }); // TODO: We currently can’t test finding rooms on other homeservers/other protocols // We obviously don’t have federation or bridges in local e2e tests - test.skip("should find unknown public rooms on other homeservers", async ({ page, app }) => { + test.skip("should find unknown public rooms on other homeservers", async ({ page, app, room3 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.PublicRooms); - await spotlight.search(room3Name); + await spotlight.search(room3.name); await page.locator("[aria-haspopup=true][role=button]").click(); await page @@ -194,20 +202,20 @@ test.describe("Spotlight", () => { const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(room3Name); - await expect(resultLocator.first()).toContainText(room3Id); + await expect(resultLocator.first()).toContainText(room3.name); + await expect(resultLocator.first()).toContainText(room3.roomId); }); - test("should find known people", async ({ page, app }) => { + test("should find known people", async ({ page, app, bot1 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.People); - await spotlight.search(bot1Name); + await spotlight.search(bot1.credentials.displayName); const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(bot1Name); + await expect(resultLocator.first()).toContainText(bot1.credentials.displayName); await resultLocator.first().click(); - await expect(roomHeaderName(page)).toHaveText(bot1Name); + await expect(roomHeaderName(page)).toHaveText(bot1.credentials.displayName); }); /** @@ -217,42 +225,41 @@ test.describe("Spotlight", () => { * * https://github.com/matrix-org/synapse/issues/16472 */ - test("should find unknown people", async ({ page, app }) => { + test("should find unknown people", async ({ page, app, bot2 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.People); - await spotlight.search(bot2Name); + await spotlight.search(bot2.credentials.displayName); const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(bot2Name); + await expect(resultLocator.first()).toContainText(bot2.credentials.displayName); await resultLocator.first().click(); - await expect(roomHeaderName(page)).toHaveText(bot2Name); + await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName); }); - test("should find group DMs by usernames or user ids", async ({ page, app }) => { + test("should find group DMs by usernames or user ids", async ({ page, app, bot1, bot2, room1 }) => { // First we want to share a room with both bots to ensure we’ve got their usernames cached - const bot2UserId = await bot2.evaluate((client) => client.getUserId()); - await app.client.inviteUser(room1Id, bot2UserId); + await app.client.inviteUser(room1.roomId, bot2.credentials.userId); // Starting a DM with ByteBot (will be turned into a group dm later) let spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.People); - await spotlight.search(bot2Name); + await spotlight.search(bot2.credentials.displayName); let resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(bot2Name); + await expect(resultLocator.first()).toContainText(bot2.credentials.displayName); await resultLocator.first().click(); // Send first message to actually start DM - await expect(roomHeaderName(page)).toHaveText(bot2Name); + await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName); const locator = page.getByRole("textbox", { name: "Send a message…" }); await locator.fill("Hey!"); await locator.press("Enter"); // Assert DM exists by checking for the first message and the room being in the room list await expect(page.locator(".mx_EventTile_body").filter({ hasText: "Hey!" })).toBeAttached({ timeout: 3000 }); - await expect(page.getByRole("group", { name: "People" })).toContainText(bot2Name); + await expect(page.getByRole("group", { name: "People" })).toContainText(bot2.credentials.displayName); // Invite BotBob into existing DM with ByteBot const dmRooms = await app.client.evaluate((client, userId) => { @@ -260,18 +267,17 @@ test.describe("Spotlight", () => { .getAccountData("m.direct" as keyof AccountDataEvents) ?.getContent>(); return map[userId] ?? []; - }, bot2UserId); + }, bot2.credentials.userId); expect(dmRooms).toHaveLength(1); const groupDmName = await app.client.evaluate((client, id) => client.getRoom(id).name, dmRooms[0]); - const bot1UserId = await bot1.evaluate((client) => client.getUserId()); - await app.client.inviteUser(dmRooms[0], bot1UserId); + await app.client.inviteUser(dmRooms[0], bot1.credentials.userId); await expect(roomHeaderName(page).first()).toContainText(groupDmName); await expect(page.getByRole("group", { name: "People" }).first()).toContainText(groupDmName); // Search for BotBob by id, should return group DM and user spotlight = await app.openSpotlight(); await spotlight.filter(Filter.People); - await spotlight.search(bot1UserId); + await spotlight.search(bot1.credentials.userId); await page.waitForTimeout(1000); // wait for the dialog to settle resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(2); @@ -284,7 +290,7 @@ test.describe("Spotlight", () => { // Search for ByteBot by id, should return group DM and user spotlight = await app.openSpotlight(); await spotlight.filter(Filter.People); - await spotlight.search(bot2UserId); + await spotlight.search(bot2.credentials.userId); await page.waitForTimeout(1000); // wait for the dialog to settle resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(2); @@ -297,11 +303,10 @@ test.describe("Spotlight", () => { }); // Test against https://github.com/vector-im/element-web/issues/22851 - test("should show each person result only once", async ({ page, app }) => { + test("should show each person result only once", async ({ page, app, bot1 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.People); - const bot1UserId = await bot1.evaluate((client) => client.getUserId()); // 2 rounds of search to simulate the bug conditions. Specifically, the first search // should have 1 result (not 2) and the second search should also have 1 result (instead @@ -310,24 +315,24 @@ test.describe("Spotlight", () => { // We search for user ID to trigger the profile lookup within the dialog. for (let i = 0; i < 2; i++) { console.log("Iteration: " + i); - await spotlight.search(bot1UserId); + await spotlight.search(bot1.credentials.userId); await page.waitForTimeout(1000); // wait for the dialog to settle const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(bot1UserId); + await expect(resultLocator.first()).toContainText(bot1.credentials.userId); } }); - test("should allow opening group chat dialog", async ({ page, app }) => { + test("should allow opening group chat dialog", async ({ page, app, bot2 }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.People); - await spotlight.search(bot2Name); + await spotlight.search(bot2.credentials.displayName); await page.waitForTimeout(3000); // wait for the dialog to settle const resultLocator = spotlight.results; await expect(resultLocator).toHaveCount(1); - await expect(resultLocator.first()).toContainText(bot2Name); + await expect(resultLocator.first()).toContainText(bot2.credentials.displayName); await expect(spotlight.dialog.locator(".mx_SpotlightDialog_startGroupChat")).toContainText( "Start a group chat", @@ -336,18 +341,18 @@ test.describe("Spotlight", () => { await expect(page.getByRole("dialog")).toContainText("Direct Messages"); }); - test("should close spotlight after starting a DM", async ({ page, app }) => { - await startDM(app, page, bot1Name); + test("should close spotlight after starting a DM", async ({ page, app, bot1 }) => { + await startDM(app, page, bot1.credentials.displayName); await expect(page.locator(".mx_SpotlightDialog")).toHaveCount(0); }); - test("should show the same user only once", async ({ page, app }) => { - await startDM(app, page, bot1Name); + test("should show the same user only once", async ({ page, app, bot1 }) => { + await startDM(app, page, bot1.credentials.displayName); await page.goto("/#/home"); const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.People); - await spotlight.search(bot1Name); + await spotlight.search(bot1.credentials.displayName); await page.waitForTimeout(3000); // wait for the dialog to settle await expect(spotlight.dialog.locator(".mx_Spinner")).not.toBeAttached(); const resultLocator = spotlight.results; From c25c8a3b9acf36ea1087f672f7ccbccb99d19e7f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 11:48:26 +0000 Subject: [PATCH 63/93] Deflake more tests using existing username Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups-mas.spec.ts | 17 +++++++++++------ playwright/e2e/login/login-sso.spec.ts | 4 ++-- playwright/e2e/login/soft_logout_oauth.spec.ts | 4 ++-- playwright/e2e/login/utils.ts | 5 +++-- playwright/e2e/oidc/oidc-native.spec.ts | 10 ++++++++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts index 614bde50646..1838f9e234b 100644 --- a/playwright/e2e/crypto/backups-mas.spec.ts +++ b/playwright/e2e/crypto/backups-mas.spec.ts @@ -19,19 +19,19 @@ test.use(masHomeserver); test.describe("Encryption state after registration", () => { test.skip(isDendrite, "does not yet support MAS"); - test("Key backup is enabled by default", async ({ page, mailhogClient, app }) => { + test("Key backup is enabled by default", async ({ page, mailhogClient, app }, testInfo) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!"); await app.settings.openUserSettings("Security & Privacy"); await expect(page.getByText("This session is backing up your keys.")).toBeVisible(); }); - test("user is prompted to set up recovery", async ({ page, mailhogClient, app }) => { + test("user is prompted to set up recovery", async ({ page, mailhogClient, app }, testInfo) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!"); await page.getByRole("button", { name: "Add room" }).click(); await page.getByRole("menuitem", { name: "New room" }).click(); @@ -45,8 +45,13 @@ test.describe("Encryption state after registration", () => { test.describe("Key backup reset from elsewhere", () => { test.skip(isDendrite, "does not yet support MAS"); - test("Key backup is disabled when reset from elsewhere", async ({ page, mailhogClient, request, homeserver }) => { - const testUsername = "alice"; + test("Key backup is disabled when reset from elsewhere", async ({ + page, + mailhogClient, + request, + homeserver, + }, testInfo) => { + const testUsername = `alice_${testInfo.testId}`; const testPassword = "Pa$sW0rD!"; // there's a delay before keys are uploaded so the error doesn't appear immediately: use a fake diff --git a/playwright/e2e/login/login-sso.spec.ts b/playwright/e2e/login/login-sso.spec.ts index fbe190b9358..22428af5028 100644 --- a/playwright/e2e/login/login-sso.spec.ts +++ b/playwright/e2e/login/login-sso.spec.ts @@ -17,13 +17,13 @@ test.use(legacyOAuthHomeserver); test.describe("SSO login", () => { test.skip(isDendrite, "does not yet support SSO"); - test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => { + test("logs in with SSO and lands on the home screen", async ({ page, homeserver }, testInfo) => { // If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to // your firewall settings: Synapse is unable to reach the OIDC server. // // If you are using ufw, try something like: // sudo ufw allow in on docker0 // - await doTokenRegistration(page, homeserver); + await doTokenRegistration(page, homeserver, testInfo); }); }); diff --git a/playwright/e2e/login/soft_logout_oauth.spec.ts b/playwright/e2e/login/soft_logout_oauth.spec.ts index 19b1fc0124c..f6814d0cf4a 100644 --- a/playwright/e2e/login/soft_logout_oauth.spec.ts +++ b/playwright/e2e/login/soft_logout_oauth.spec.ts @@ -26,8 +26,8 @@ test.use({ test.use(legacyOAuthHomeserver); test.describe("Soft logout with SSO user", () => { test.use({ - user: async ({ page, homeserver }, use) => { - const user = await doTokenRegistration(page, homeserver); + user: async ({ page, homeserver }, use, testInfo) => { + const user = await doTokenRegistration(page, homeserver, testInfo); // Eventually, we should end up at the home screen. await expect(page).toHaveURL(/\/#\/home$/); diff --git a/playwright/e2e/login/utils.ts b/playwright/e2e/login/utils.ts index cc98d8819a2..e7121159f0f 100644 --- a/playwright/e2e/login/utils.ts +++ b/playwright/e2e/login/utils.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { Page, expect } from "@playwright/test"; +import { Page, expect, TestInfo } from "@playwright/test"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; @@ -15,6 +15,7 @@ import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; export async function doTokenRegistration( page: Page, homeserver: HomeserverInstance, + testInfo: TestInfo, ): Promise { await page.goto("/#/login"); @@ -35,7 +36,7 @@ export async function doTokenRegistration( // Synapse prompts us to pick a user ID await expect(page.getByRole("heading", { name: "Create your account" })).toBeVisible(); - await page.getByRole("textbox", { name: "Username (required)" }).fill("alice"); + await page.getByRole("textbox", { name: "Username (required)" }).fill(`alice_${testInfo.testId}`); // wait for username validation to start, and complete await expect(page.locator("#field-username-output")).toHaveText(""); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 63cf0a5b59f..eb268c5cccc 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -17,7 +17,13 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { test.skip(isDendrite, "does not yet support MAS"); test.slow(); // trace recording takes a while here - test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => { + test("can register the oauth2 client and an account", async ({ + context, + page, + homeserver, + mailhogClient, + mas, + }, testInfo) => { const tokenUri = `${mas.baseUrl}/oauth2/token`; const tokenApiPromise = page.waitForRequest( (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", @@ -25,7 +31,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!"); // Eventually, we should end up at the home screen. await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); From db47e6890332f1aadc5d8d86e73b64505b99664b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 11:48:37 +0000 Subject: [PATCH 64/93] Clean mailhog between tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/services.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playwright/services.ts b/playwright/services.ts index 1e2501dc2de..a083304fa13 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -131,11 +131,12 @@ export const test = base.extend<{}, Services>({ { scope: "worker" }, ], - context: async ({ logger, context, request, homeserver }, use, testInfo) => { + context: async ({ logger, context, request, homeserver, mailhogClient }, use, testInfo) => { homeserver.setRequest(request); await logger.testStarted(testInfo); await use(context); await logger.testFinished(testInfo); await homeserver.onTestFinished(testInfo); + await mailhogClient.deleteAll(); }, }); From 8f68dbb4f340214fa3fc9c0aac7ad9789108dc01 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 12:51:32 +0000 Subject: [PATCH 65/93] Fix more flakes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/spotlight/spotlight.spec.ts | 10 +- playwright/services.ts | 2 + .../testcontainers/HomeserverContainer.ts | 2 + playwright/testcontainers/dendrite.ts | 2 +- playwright/testcontainers/mas.ts | 91 ++++++++++++--- playwright/testcontainers/synapse.ts | 105 ++++++------------ playwright/testcontainers/utils.ts | 68 +++++++++++- 7 files changed, 189 insertions(+), 91 deletions(-) diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index da35ca57b35..d4451b9b5c1 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -366,28 +366,28 @@ test.describe("Spotlight", () => { await spotlight.search("b"); let resultLocator = spotlight.results; - await expect(resultLocator).toHaveCount(2); + await expect(resultLocator.count()).resolves.toBeGreaterThan(2); await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true"); await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); await spotlight.searchBox.press("ArrowDown"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); - await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true"); + await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true"); await spotlight.searchBox.press("ArrowDown"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); - await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); + await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false"); await spotlight.searchBox.press("ArrowUp"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); - await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true"); + await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true"); await spotlight.searchBox.press("ArrowUp"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true"); - await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); + await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false"); }); }); diff --git a/playwright/services.ts b/playwright/services.ts index a083304fa13..4fe05b1282d 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -116,6 +116,8 @@ export const test = base.extend<{}, Services>({ .withConfig(synapseConfigOptions) .start(); + container.setMatrixAuthenticationService(mas); + await use(container); await container.stop(); }, diff --git a/playwright/testcontainers/HomeserverContainer.ts b/playwright/testcontainers/HomeserverContainer.ts index 73c9882418c..e825b6e5544 100644 --- a/playwright/testcontainers/HomeserverContainer.ts +++ b/playwright/testcontainers/HomeserverContainer.ts @@ -9,6 +9,7 @@ import { AbstractStartedContainer, GenericContainer } from "testcontainers"; import { APIRequestContext, TestInfo } from "@playwright/test"; import { HomeserverInstance } from "../plugins/homeserver"; +import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; export interface HomeserverContainer extends GenericContainer { withConfigField(key: string, value: any): this; @@ -18,5 +19,6 @@ export interface HomeserverContainer extends GenericContainer { export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance { setRequest(request: APIRequestContext): void; + setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void; onTestFinished(testInfo: TestInfo): Promise; } diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index 517b0f7a642..ce786d15c18 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -258,5 +258,5 @@ export class PineconeContainer extends DendriteContainer { } } -// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it +// Surprisingly, Dendrite implements the same register user Synapse Admin API, so we can just extend it export class StartedDendriteContainer extends StartedSynapseContainer {} diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index d15f619dbc6..e60cb652982 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -11,6 +11,8 @@ import * as YAML from "yaml"; import { getFreePort } from "../plugins/utils/port.ts"; import { deepCopy } from "../plugins/utils/object.ts"; +import { Credentials } from "../plugins/homeserver"; +import { ClientServerApi } from "./utils.ts"; const DEFAULT_CONFIG = { http: { @@ -18,18 +20,11 @@ const DEFAULT_CONFIG = { { name: "web", resources: [ - { - name: "discovery", - }, - { - name: "human", - }, - { - name: "oauth", - }, - { - name: "compat", - }, + { name: "discovery" }, + { name: "human" }, + { name: "oauth" }, + { name: "compat" }, + { name: "adminapi" }, { name: "graphql", playground: true, @@ -172,9 +167,12 @@ const DEFAULT_CONFIG = { export class MatrixAuthenticationServiceContainer extends GenericContainer { private config: typeof DEFAULT_CONFIG; + private readonly args = ["-c", "/config/config.yaml"]; constructor(db: StartedPostgreSqlContainer) { - super("ghcr.io/element-hq/matrix-authentication-service:0.12.0"); + // We rely on `mas-cli manage add-email` which isn't in a release yet + // https://github.com/element-hq/matrix-authentication-service/pull/3235 + super("ghcr.io/element-hq/matrix-authentication-service:sha-0b90c33"); this.config = deepCopy(DEFAULT_CONFIG); this.config.database.username = db.getUsername(); @@ -182,7 +180,7 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer { this.withExposedPorts(8080, 8081) .withWaitStrategy(Wait.forHttp("/health", 8081)) - .withCommand(["server", "--config", "/config/config.yaml"]); + .withCommand(["server", ...this.args]); } public withConfig(config: object): this { @@ -210,15 +208,78 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer { }, ]); - return new StartedMatrixAuthenticationServiceContainer(await super.start(), `http://localhost:${port}`); + return new StartedMatrixAuthenticationServiceContainer( + await super.start(), + `http://localhost:${port}`, + this.args, + ); } } export class StartedMatrixAuthenticationServiceContainer extends AbstractStartedContainer { + private adminTokenPromise?: Promise; + constructor( container: StartedTestContainer, public readonly baseUrl: string, + private readonly args: string[], ) { super(container); } + + public async getAdminToken(csApi: ClientServerApi): Promise { + if (this.adminTokenPromise === undefined) { + this.adminTokenPromise = this.registerUserInternal( + csApi, + "admin", + "totalyinsecureadminpassword", + undefined, + true, + ).then((res) => res.accessToken); + } + return this.adminTokenPromise; + } + + private async registerUserInternal( + csApi: ClientServerApi, + username: string, + password: string, + displayName?: string, + admin = false, + ): Promise { + const args: string[] = []; + if (admin) args.push("-a"); + await this.exec([ + "mas-cli", + "manage", + "register-user", + ...this.args, + ...args, + "-y", + "-p", + password, + "-d", + displayName ?? "", + username, + ]); + + return csApi.loginUser(username, password); + } + + public async registerUser( + csApi: ClientServerApi, + username: string, + password: string, + displayName?: string, + ): Promise { + return this.registerUserInternal(csApi, username, password, displayName, false); + } + + public async setThreepid(username: string, medium: string, address: string): Promise { + if (medium !== "email") { + throw new Error("Only email threepids are supported by MAS"); + } + + await this.exec(["mas-cli", "manage", "add-email", ...this.args, username, address]); + } } diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 27249b75952..05b5702328a 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -16,6 +16,8 @@ import { randB64Bytes } from "../plugins/utils/rand.ts"; import { Credentials } from "../plugins/homeserver"; import { deepCopy } from "../plugins/utils/object.ts"; import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverContainer.ts"; +import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; +import { Api, ClientServerApi, Verb } from "./utils.ts"; const TAG = "develop@sha256:b69222d98abe9625d46f5d3cb01683d5dc173ae339215297138392cfeec935d9"; @@ -138,8 +140,6 @@ const DEFAULT_CONFIG = { }, }; -type Verb = "GET" | "POST" | "PUT" | "DELETE"; - export type SynapseConfigOptions = Partial; export class SynapseContainer extends GenericContainer implements HomeserverContainer { @@ -231,7 +231,10 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { private adminTokenPromise?: Promise; + private _mas?: StartedMatrixAuthenticationServiceContainer; protected _request?: APIRequestContext; + protected csApi: ClientServerApi; + protected adminApi: Api; constructor( container: StartedTestContainer, @@ -239,10 +242,17 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements private readonly registrationSharedSecret: string, ) { super(container); + this.csApi = new ClientServerApi(this.baseUrl); + this.adminApi = new Api(`${this.baseUrl}/_synapse/admin/`); } public setRequest(request: APIRequestContext): void { this._request = request; + this.csApi.setRequest(request); + } + + public setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void { + this._mas = mas; } public async onTestFinished(testInfo: TestInfo): Promise { @@ -251,13 +261,14 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements } protected async deletePublicRooms(): Promise { + const token = await this.getAdminToken(); // We hide the rooms from the room directory to save time between tests and for portability between homeservers - const { chunk: rooms } = await this.request<{ + const { chunk: rooms } = await this.csApi.request<{ chunk: { room_id: string }[]; - }>("GET", "v3/publicRooms", {}); + }>("GET", "v3/publicRooms", token, {}); await Promise.all( rooms.map((room) => - this.request("PUT", `v3/directory/list/room/${room.room_id}`, { visibility: "private" }), + this.csApi.request("PUT", `v3/directory/list/room/${room.room_id}`, token, { visibility: "private" }), ), ); } @@ -268,13 +279,18 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements displayName?: string, admin = false, ): Promise { - const url = `${this.baseUrl}/_synapse/admin/v1/register`; - const { nonce } = await this._request.get(url).then((r) => r.json()); + const path = `v1/register`; + const { nonce } = await this.adminApi.request<{ nonce: string }>("GET", path, undefined, {}); const mac = crypto .createHmac("sha1", this.registrationSharedSecret) .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) .digest("hex"); - const res = await this._request.post(url, { + const data = await this.adminApi.request<{ + home_server: string; + access_token: string; + user_id: string; + device_id: string; + }>("POST", path, undefined, { data: { nonce, username, @@ -285,11 +301,6 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements }, }); - if (!res.ok()) { - throw await res.json(); - } - - const data = await res.json(); return { homeServer: data.home_server, accessToken: data.access_token, @@ -303,6 +314,10 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements protected async getAdminToken(): Promise { if (this.adminTokenPromise === undefined) { + if (this._mas) { + return (this.adminTokenPromise = this._mas.getAdminToken(this.csApi)); + } + this.adminTokenPromise = this.registerUserInternal( "admin", "totalyinsecureadminpassword", @@ -317,72 +332,24 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements private async adminRequest(verb: Verb, path: string, data?: object): Promise; private async adminRequest(verb: Verb, path: string, data?: object): Promise { const adminToken = await this.getAdminToken(); - const url = `${this.baseUrl}/_synapse/admin/${path}`; - const res = await this._request.fetch(url, { - data, - method: verb, - headers: { - Authorization: `Bearer ${adminToken}`, - }, - }); - - if (!res.ok()) { - throw await res.json(); - } - - return res.json(); - } - - public async request(verb: "GET", path: string, data?: never): Promise; - public async request(verb: Verb, path: string, data?: object): Promise; - public async request(verb: Verb, path: string, data?: object): Promise { - const token = await this.getAdminToken(); - const url = `${this.baseUrl}/_matrix/client/${path}`; - const res = await this._request.fetch(url, { - data, - method: verb, - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!res.ok()) { - throw await res.json(); - } - - return res.json(); + return this.adminApi.request(verb, path, adminToken, data); } public registerUser(username: string, password: string, displayName?: string): Promise { + if (this._mas) { + return this._mas.registerUser(this.csApi, username, password, displayName); + } return this.registerUserInternal(username, password, displayName, false); } public async loginUser(userId: string, password: string): Promise { - const json = await this.request<{ - access_token: string; - user_id: string; - device_id: string; - home_server: string; - }>("POST", "v3/login", { - type: "m.login.password", - identifier: { - type: "m.id.user", - user: userId, - }, - password: password, - }); - - return { - password, - accessToken: json.access_token, - userId: json.user_id, - deviceId: json.device_id, - homeServer: json.home_server, - username: userId.slice(1).split(":")[0], - }; + return this.csApi.loginUser(userId, password); } public async setThreepid(userId: string, medium: string, address: string): Promise { + if (this._mas) { + return this._mas.setThreepid(userId, medium, address); + } await this.adminRequest("PUT", `v2/users/${userId}`, { threepids: [ { diff --git a/playwright/testcontainers/utils.ts b/playwright/testcontainers/utils.ts index 1339e9c2fc1..487562f8af9 100644 --- a/playwright/testcontainers/utils.ts +++ b/playwright/testcontainers/utils.ts @@ -5,10 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { TestInfo } from "@playwright/test"; +import { APIRequestContext, TestInfo } from "@playwright/test"; import { Readable } from "stream"; import stripAnsi from "strip-ansi"; +import { Credentials } from "../plugins/homeserver"; + export class ContainerLogger { private logs: Record = {}; @@ -41,3 +43,67 @@ export class ContainerLogger { } } } + +export type Verb = "GET" | "POST" | "PUT" | "DELETE"; + +export class Api { + private _request?: APIRequestContext; + + constructor(private readonly baseUrl: string) {} + + public setRequest(request: APIRequestContext): void { + this._request = request; + } + + public async request(verb: "GET", path: string, token?: string, data?: never): Promise; + public async request(verb: Verb, path: string, token?: string, data?: object): Promise; + public async request(verb: Verb, path: string, token?: string, data?: object): Promise { + const url = `${this.baseUrl}/_matrix/client/${path}`; + const res = await this._request.fetch(url, { + data, + method: verb, + headers: token + ? { + Authorization: `Bearer ${token}`, + } + : undefined, + }); + + if (!res.ok()) { + throw await res.json(); + } + + return res.json(); + } +} + +export class ClientServerApi extends Api { + constructor(baseUrl: string) { + super(`${baseUrl}/_matrix/client/`); + } + + public async loginUser(userId: string, password: string): Promise { + const json = await this.request<{ + access_token: string; + user_id: string; + device_id: string; + home_server: string; + }>("POST", "v3/login", undefined, { + type: "m.login.password", + identifier: { + type: "m.id.user", + user: userId, + }, + password: password, + }); + + return { + password, + accessToken: json.access_token, + userId: json.user_id, + deviceId: json.device_id, + homeServer: json.home_server, + username: userId.slice(1).split(":")[0], + }; + } +} From 548d45af886600bda199db20936ac0b4cacfe1c4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 13:17:58 +0000 Subject: [PATCH 66/93] Fix missing _request Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/synapse.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 05b5702328a..ff1b0002327 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -249,6 +249,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements public setRequest(request: APIRequestContext): void { this._request = request; this.csApi.setRequest(request); + this.adminApi.setRequest(request); } public setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void { From 8b3ffb4b3fc07c464bc366348c8d9a2ac1253bb9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 13:59:18 +0000 Subject: [PATCH 67/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/synapse.ts | 24 +++++++++++------------- playwright/testcontainers/utils.ts | 6 +++--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index ff1b0002327..8c5fafdbd5d 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -243,7 +243,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements ) { super(container); this.csApi = new ClientServerApi(this.baseUrl); - this.adminApi = new Api(`${this.baseUrl}/_synapse/admin/`); + this.adminApi = new Api(`${this.baseUrl}/_synapse/admin`); } public setRequest(request: APIRequestContext): void { @@ -266,10 +266,10 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements // We hide the rooms from the room directory to save time between tests and for portability between homeservers const { chunk: rooms } = await this.csApi.request<{ chunk: { room_id: string }[]; - }>("GET", "v3/publicRooms", token, {}); + }>("GET", "/v3/publicRooms", token, {}); await Promise.all( rooms.map((room) => - this.csApi.request("PUT", `v3/directory/list/room/${room.room_id}`, token, { visibility: "private" }), + this.csApi.request("PUT", `/v3/directory/list/room/${room.room_id}`, token, { visibility: "private" }), ), ); } @@ -280,7 +280,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements displayName?: string, admin = false, ): Promise { - const path = `v1/register`; + const path = "/v1/register"; const { nonce } = await this.adminApi.request<{ nonce: string }>("GET", path, undefined, {}); const mac = crypto .createHmac("sha1", this.registrationSharedSecret) @@ -292,14 +292,12 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements user_id: string; device_id: string; }>("POST", path, undefined, { - data: { - nonce, - username, - password, - mac, - admin, - displayname: displayName, - }, + nonce, + username, + password, + mac, + admin, + displayname: displayName, }); return { @@ -351,7 +349,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements if (this._mas) { return this._mas.setThreepid(userId, medium, address); } - await this.adminRequest("PUT", `v2/users/${userId}`, { + await this.adminRequest("PUT", `/v2/users/${userId}`, { threepids: [ { medium, diff --git a/playwright/testcontainers/utils.ts b/playwright/testcontainers/utils.ts index 487562f8af9..c48c655c2e5 100644 --- a/playwright/testcontainers/utils.ts +++ b/playwright/testcontainers/utils.ts @@ -58,7 +58,7 @@ export class Api { public async request(verb: "GET", path: string, token?: string, data?: never): Promise; public async request(verb: Verb, path: string, token?: string, data?: object): Promise; public async request(verb: Verb, path: string, token?: string, data?: object): Promise { - const url = `${this.baseUrl}/_matrix/client/${path}`; + const url = `${this.baseUrl}${path}`; const res = await this._request.fetch(url, { data, method: verb, @@ -79,7 +79,7 @@ export class Api { export class ClientServerApi extends Api { constructor(baseUrl: string) { - super(`${baseUrl}/_matrix/client/`); + super(`${baseUrl}/_matrix/client`); } public async loginUser(userId: string, password: string): Promise { @@ -88,7 +88,7 @@ export class ClientServerApi extends Api { user_id: string; device_id: string; home_server: string; - }>("POST", "v3/login", undefined, { + }>("POST", "/v3/login", undefined, { type: "m.login.password", identifier: { type: "m.id.user", From 71f06cd2588700cfbb68b7ba84a37aaf24df5726 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 17:51:40 +0000 Subject: [PATCH 68/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/csAPI.ts | 29 +++---- playwright/e2e/login/login-consent.spec.ts | 6 ++ playwright/e2e/oidc/index.ts | 2 +- playwright/e2e/oidc/oidc-native.spec.ts | 9 +- .../e2e/sliding-sync/sliding-sync.spec.ts | 1 - playwright/e2e/spotlight/spotlight.spec.ts | 10 +-- playwright/element-web-test.ts | 2 +- playwright/plugins/homeserver/index.ts | 3 + playwright/services.ts | 3 +- .../testcontainers/HomeserverContainer.ts | 2 +- playwright/testcontainers/dendrite.ts | 5 ++ playwright/testcontainers/mas.ts | 80 +++++++++++++---- playwright/testcontainers/synapse.ts | 86 +++++++++++++------ playwright/testcontainers/utils.ts | 4 +- 14 files changed, 167 insertions(+), 75 deletions(-) diff --git a/playwright/e2e/csAPI.ts b/playwright/e2e/csAPI.ts index d55816fb6a2..78362506ef2 100644 --- a/playwright/e2e/csAPI.ts +++ b/playwright/e2e/csAPI.ts @@ -9,24 +9,24 @@ import { APIRequestContext } from "playwright-core"; import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { HomeserverInstance } from "../plugins/homeserver"; +import { ClientServerApi } from "../testcontainers/utils.ts"; /** * A small subset of the Client-Server API used to manipulate the state of the * account on the homeserver independently of the client under test. */ -export class TestClientServerAPI { +export class TestClientServerAPI extends ClientServerApi { public constructor( - private request: APIRequestContext, - private homeserver: HomeserverInstance, + request: APIRequestContext, + homeserver: HomeserverInstance, private accessToken: string, - ) {} + ) { + super(`${homeserver.baseUrl}/_matrix/client`); + this.setRequest(request); + } public async getCurrentBackupInfo(): Promise { - const res = await this.request.get(`${this.homeserver.baseUrl}/_matrix/client/v3/room_keys/version`, { - headers: { Authorization: `Bearer ${this.accessToken}` }, - }); - - return await res.json(); + return this.request("GET", `/v3/room_keys/version`, this.accessToken); } /** @@ -34,15 +34,6 @@ export class TestClientServerAPI { * @param version The version to delete */ public async deleteBackupVersion(version: string): Promise { - const res = await this.request.delete( - `${this.homeserver.baseUrl}/_matrix/client/v3/room_keys/version/${version}`, - { - headers: { Authorization: `Bearer ${this.accessToken}` }, - }, - ); - - if (!res.ok) { - throw new Error(`Failed to delete backup version: ${res.status}`); - } + await this.request("DELETE", `/v3/room_keys/version/${version}`, this.accessToken); } } diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index ab70e1d1869..d7d5861a02e 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -77,6 +77,9 @@ async function login(page: Page, homeserver: HomeserverInstance, credentials: Cr await page.getByRole("button", { name: "Sign in" }).click(); } +// This test suite uses the same userId for all tests in the suite +// due to DEVICE_SIGNING_KEYS_BODY being specific to that userId, +// so we restart the Synapse container to make it forget everything. test.use(consentHomeserver); test.use({ config: { @@ -97,6 +100,9 @@ test.use({ ...credentials, displayName, }); + + // Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts + await homeserver.restart(); }, }); diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 7e9b03ee6ac..bfd49b496a0 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -33,7 +33,7 @@ export async function registerAccountMas( expect(messages.items).toHaveLength(1); }).toPass(); expect(messages.items[0].to).toEqual(`${username} <${email}>`); - const [code] = messages.items[0].text.match(/(\d{6})/); + const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/); await page.getByRole("textbox", { name: "6-digit code" }).fill(code); await page.getByRole("button", { name: "Continue" }).click(); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index eb268c5cccc..a50730ce747 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -24,6 +24,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { mailhogClient, mas, }, testInfo) => { + await page.clock.install(); + const tokenUri = `${mas.baseUrl}/oauth2/token`; const tokenApiPromise = page.waitForRequest( (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", @@ -31,11 +33,14 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!"); + + const userId = `alice_${testInfo.testId}`; + await registerAccountMas(page, mailhogClient, userId, "alice@email.com", "Pa$sW0rD!"); // Eventually, we should end up at the home screen. await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); - await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible(); + await expect(page.getByRole("heading", { name: `Welcome ${userId}`, exact: true })).toBeVisible(); + await page.clock.runFor(20000); // run the timer so we see the token request const tokenApiRequest = await tokenApiPromise; expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code"); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 29a612ccd32..1ab7909a478 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -108,7 +108,6 @@ test.describe("Sliding Sync", () => { await page.getByRole("menuitemradio", { name: "A-Z" }).dispatchEvent("click"); await expect(page.locator(".mx_StyledRadioButton_checked").getByText("A-Z")).toBeVisible(); - await page.pause(); await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page); }); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index d4451b9b5c1..da35ca57b35 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -366,28 +366,28 @@ test.describe("Spotlight", () => { await spotlight.search("b"); let resultLocator = spotlight.results; - await expect(resultLocator.count()).resolves.toBeGreaterThan(2); + await expect(resultLocator).toHaveCount(2); await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true"); await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); await spotlight.searchBox.press("ArrowDown"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); - await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true"); await spotlight.searchBox.press("ArrowDown"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); - await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); await spotlight.searchBox.press("ArrowUp"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false"); - await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true"); await spotlight.searchBox.press("ArrowUp"); resultLocator = spotlight.results; await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true"); - await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false"); + await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); }); }); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 0c6392fdc21..12b86c1f69f 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -41,7 +41,7 @@ const CONFIG_JSON: Partial = { }, }; -interface CredentialsWithDisplayName extends Credentials { +export interface CredentialsWithDisplayName extends Credentials { displayName: string; } diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index 50dea472f78..9e54e0aa916 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -6,8 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { ClientServerApi } from "../../testcontainers/utils.ts"; + export interface HomeserverInstance { readonly baseUrl: string; + readonly csApi: ClientServerApi; /** * Register a user on the given Homeserver using the shared registration secret. diff --git a/playwright/services.ts b/playwright/services.ts index 4fe05b1282d..0d9ece05291 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -114,10 +114,9 @@ export const test = base.extend<{}, Services>({ .withNetworkAliases("homeserver") .withLogConsumer(logger.getConsumer("synapse")) .withConfig(synapseConfigOptions) + .withMatrixAuthenticationService(mas) .start(); - container.setMatrixAuthenticationService(mas); - await use(container); await container.stop(); }, diff --git a/playwright/testcontainers/HomeserverContainer.ts b/playwright/testcontainers/HomeserverContainer.ts index e825b6e5544..259ecb7fe0a 100644 --- a/playwright/testcontainers/HomeserverContainer.ts +++ b/playwright/testcontainers/HomeserverContainer.ts @@ -14,11 +14,11 @@ import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; export interface HomeserverContainer extends GenericContainer { withConfigField(key: string, value: any): this; withConfig(config: Partial): this; + withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this; start(): Promise; } export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance { setRequest(request: APIRequestContext): void; - setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void; onTestFinished(testInfo: TestInfo): Promise; } diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index ce786d15c18..c358ff15852 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -13,6 +13,7 @@ import { randB64Bytes } from "../plugins/utils/rand.ts"; import { StartedSynapseContainer } from "./synapse.ts"; import { deepCopy } from "../plugins/utils/object.ts"; import { HomeserverContainer } from "./HomeserverContainer.ts"; +import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; const DEFAULT_CONFIG = { version: 2, @@ -235,6 +236,10 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon return this; } + public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this { + throw new Error("Dendrite does not support MAS."); + } + public override async start(): Promise { this.withCopyContentToContainer([ { diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index e60cb652982..697ef374a18 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -5,14 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait, ExecResult } from "testcontainers"; import { StartedPostgreSqlContainer } from "@testcontainers/postgresql"; import * as YAML from "yaml"; import { getFreePort } from "../plugins/utils/port.ts"; import { deepCopy } from "../plugins/utils/object.ts"; import { Credentials } from "../plugins/homeserver"; -import { ClientServerApi } from "./utils.ts"; const DEFAULT_CONFIG = { http: { @@ -227,10 +226,9 @@ export class StartedMatrixAuthenticationServiceContainer extends AbstractStarted super(container); } - public async getAdminToken(csApi: ClientServerApi): Promise { + public async getAdminToken(): Promise { if (this.adminTokenPromise === undefined) { this.adminTokenPromise = this.registerUserInternal( - csApi, "admin", "totalyinsecureadminpassword", undefined, @@ -240,20 +238,24 @@ export class StartedMatrixAuthenticationServiceContainer extends AbstractStarted return this.adminTokenPromise; } - private async registerUserInternal( - csApi: ClientServerApi, + private async manage(cmd: string, ...args: string[]): Promise { + const result = await this.exec(["mas-cli", "manage", cmd, ...this.args, ...args]); + if (result.exitCode !== 0) { + throw new Error(`Failed mas-cli manage ${cmd}: ${result.output}`); + } + return result; + } + + private async manageRegisterUser( username: string, password: string, displayName?: string, admin = false, - ): Promise { + ): Promise { const args: string[] = []; if (admin) args.push("-a"); - await this.exec([ - "mas-cli", - "manage", + const result = await this.manage( "register-user", - ...this.args, ...args, "-y", "-p", @@ -261,18 +263,62 @@ export class StartedMatrixAuthenticationServiceContainer extends AbstractStarted "-d", displayName ?? "", username, - ]); + ); - return csApi.loginUser(username, password); + const registerLines = result.output.trim().split("\n"); + const userId = registerLines + .find((line) => line.includes("Matrix ID: ")) + ?.split(": ") + .pop(); + + if (!userId) { + throw new Error(`Failed to register user: ${result.output}`); + } + + return userId; } - public async registerUser( - csApi: ClientServerApi, + private async manageIssueCompatibilityToken( + username: string, + admin = false, + ): Promise<{ accessToken: string; deviceId: string }> { + const args: string[] = []; + if (admin) args.push("--yes-i-want-to-grant-synapse-admin-privileges"); + const result = await this.manage("issue-compatibility-token", ...args, username); + + const parts = result.output.trim().split(/\s+/); + const accessToken = parts.find((part) => part.startsWith("mct_")); + const deviceId = parts.find((part) => part.startsWith("compat_session.device="))?.split("=")[1]; + + if (!accessToken || !deviceId) { + throw new Error(`Failed to issue compatibility token: ${result.output}`); + } + + return { accessToken, deviceId }; + } + + private async registerUserInternal( username: string, password: string, displayName?: string, + admin = false, ): Promise { - return this.registerUserInternal(csApi, username, password, displayName, false); + const userId = await this.manageRegisterUser(username, password, displayName, admin); + const { deviceId, accessToken } = await this.manageIssueCompatibilityToken(username, admin); + + return { + userId, + accessToken, + deviceId, + homeServer: userId.slice(1).split(":").slice(1).join(":"), + displayName, + username, + password, + }; + } + + public async registerUser(username: string, password: string, displayName?: string): Promise { + return this.registerUserInternal(username, password, displayName, false); } public async setThreepid(username: string, medium: string, address: string): Promise { @@ -280,6 +326,6 @@ export class StartedMatrixAuthenticationServiceContainer extends AbstractStarted throw new Error("Only email threepids are supported by MAS"); } - await this.exec(["mas-cli", "manage", "add-email", ...this.args, username, address]); + await this.manage("add-email", username, address); } } diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 8c5fafdbd5d..74625ca3b98 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { + AbstractStartedContainer, + GenericContainer, + ImageName, + RestartOptions, + StartedTestContainer, + Wait, +} from "testcontainers"; import { APIRequestContext, TestInfo } from "@playwright/test"; import crypto from "node:crypto"; import * as YAML from "yaml"; @@ -144,6 +151,7 @@ export type SynapseConfigOptions = Partial; export class SynapseContainer extends GenericContainer implements HomeserverContainer { private config: typeof DEFAULT_CONFIG; + private mas?: StartedMatrixAuthenticationServiceContainer; constructor() { super(`ghcr.io/element-hq/synapse:${TAG}`); @@ -203,6 +211,11 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont return this; } + public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this { + this.mas = mas; + return this; + } + public override async start(): Promise { // Synapse config public_baseurl needs to know what URL it'll be accessed from, so we have to map the port manually const port = await getFreePort(); @@ -221,20 +234,26 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont }, ]); - return new StartedSynapseContainer( - await super.start(), - `http://localhost:${port}`, - this.config.registration_shared_secret, - ); + const container = await super.start(); + const baseUrl = `http://localhost:${port}`; + if (this.mas) { + return new StartedSynapseWithMasContainer( + container, + baseUrl, + this.config.registration_shared_secret, + this.mas, + ); + } + + return new StartedSynapseContainer(container, baseUrl, this.config.registration_shared_secret); } } export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { - private adminTokenPromise?: Promise; - private _mas?: StartedMatrixAuthenticationServiceContainer; + protected adminTokenPromise?: Promise; protected _request?: APIRequestContext; - protected csApi: ClientServerApi; - protected adminApi: Api; + protected readonly adminApi: Api; + public readonly csApi: ClientServerApi; constructor( container: StartedTestContainer, @@ -242,8 +261,13 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements private readonly registrationSharedSecret: string, ) { super(container); - this.csApi = new ClientServerApi(this.baseUrl); this.adminApi = new Api(`${this.baseUrl}/_synapse/admin`); + this.csApi = new ClientServerApi(this.baseUrl); + } + + public restart(options?: Partial): Promise { + this.adminTokenPromise = undefined; + return super.restart(options); } public setRequest(request: APIRequestContext): void { @@ -252,10 +276,6 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements this.adminApi.setRequest(request); } - public setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void { - this._mas = mas; - } - public async onTestFinished(testInfo: TestInfo): Promise { // Clean up the server to prevent rooms leaking between tests await this.deletePublicRooms(); @@ -313,10 +333,6 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements protected async getAdminToken(): Promise { if (this.adminTokenPromise === undefined) { - if (this._mas) { - return (this.adminTokenPromise = this._mas.getAdminToken(this.csApi)); - } - this.adminTokenPromise = this.registerUserInternal( "admin", "totalyinsecureadminpassword", @@ -335,9 +351,6 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements } public registerUser(username: string, password: string, displayName?: string): Promise { - if (this._mas) { - return this._mas.registerUser(this.csApi, username, password, displayName); - } return this.registerUserInternal(username, password, displayName, false); } @@ -346,9 +359,6 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements } public async setThreepid(userId: string, medium: string, address: string): Promise { - if (this._mas) { - return this._mas.setThreepid(userId, medium, address); - } await this.adminRequest("PUT", `/v2/users/${userId}`, { threepids: [ { @@ -359,3 +369,29 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements }); } } + +export class StartedSynapseWithMasContainer extends StartedSynapseContainer { + constructor( + container: StartedTestContainer, + baseUrl: string, + registrationSharedSecret: string, + private readonly mas: StartedMatrixAuthenticationServiceContainer, + ) { + super(container, baseUrl, registrationSharedSecret); + } + + protected async getAdminToken(): Promise { + if (this.adminTokenPromise === undefined) { + this.adminTokenPromise = this.mas.getAdminToken(); + } + return this.adminTokenPromise; + } + + public registerUser(username: string, password: string, displayName?: string): Promise { + return this.mas.registerUser(username, password, displayName); + } + + public async setThreepid(userId: string, medium: string, address: string): Promise { + return this.mas.setThreepid(userId, medium, address); + } +} diff --git a/playwright/testcontainers/utils.ts b/playwright/testcontainers/utils.ts index c48c655c2e5..f4fe7f6d318 100644 --- a/playwright/testcontainers/utils.ts +++ b/playwright/testcontainers/utils.ts @@ -70,7 +70,9 @@ export class Api { }); if (!res.ok()) { - throw await res.json(); + throw new Error( + `Request to ${url} failed with status ${res.status()}: ${JSON.stringify(await res.json())}`, + ); } return res.json(); From 1a98f8ceadbf02c48083fb599446d444f6b1a233 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 18:07:43 +0000 Subject: [PATCH 69/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups-mas.spec.ts | 3 +-- playwright/e2e/csAPI.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts index 1838f9e234b..b51f7372558 100644 --- a/playwright/e2e/crypto/backups-mas.spec.ts +++ b/playwright/e2e/crypto/backups-mas.spec.ts @@ -67,8 +67,7 @@ test.describe("Key backup reset from elsewhere", () => { await page.getByRole("textbox", { name: "Name" }).fill("test room"); await page.getByRole("button", { name: "Create room" }).click(); - // @ts-ignore - this runs in the browser scope where mxMatrixClientPeg is a thing. Here, it is not. - const accessToken = await page.evaluate(() => mxMatrixClientPeg.get().getAccessToken()); + const accessToken = await page.evaluate(() => window.mxMatrixClientPeg.get().getAccessToken()); const csAPI = new TestClientServerAPI(request, homeserver, accessToken); diff --git a/playwright/e2e/csAPI.ts b/playwright/e2e/csAPI.ts index 78362506ef2..f171ded5e3b 100644 --- a/playwright/e2e/csAPI.ts +++ b/playwright/e2e/csAPI.ts @@ -21,7 +21,7 @@ export class TestClientServerAPI extends ClientServerApi { homeserver: HomeserverInstance, private accessToken: string, ) { - super(`${homeserver.baseUrl}/_matrix/client`); + super(homeserver.baseUrl); this.setRequest(request); } From 0f6f0471596257bb97cf8f70f884580f2c620aa6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 18:09:29 +0000 Subject: [PATCH 70/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/share-dialog/share-dialog.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playwright/e2e/share-dialog/share-dialog.spec.ts b/playwright/e2e/share-dialog/share-dialog.spec.ts index d5424a681d6..58574a46ffe 100644 --- a/playwright/e2e/share-dialog/share-dialog.spec.ts +++ b/playwright/e2e/share-dialog/share-dialog.spec.ts @@ -23,7 +23,7 @@ test.describe("Share dialog", () => { const dialog = page.getByRole("dialog", { name: "Share room" }); await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible(); - expect(dialog).toMatchScreenshot("share-dialog-room.png", { + await expect(dialog).toMatchScreenshot("share-dialog-room.png", { // QRCode and url changes at every run mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], }); @@ -40,7 +40,7 @@ test.describe("Share dialog", () => { const dialog = page.getByRole("dialog", { name: "Share User" }); await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible(); - expect(dialog).toMatchScreenshot("share-dialog-user.png", { + await expect(dialog).toMatchScreenshot("share-dialog-user.png", { // QRCode changes at every run mask: [page.locator(".mx_QRCode")], }); @@ -57,7 +57,7 @@ test.describe("Share dialog", () => { const dialog = page.getByRole("dialog", { name: "Share Room Message" }); await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); - expect(dialog).toMatchScreenshot("share-dialog-event.png", { + await expect(dialog).toMatchScreenshot("share-dialog-event.png", { // QRCode and url changes at every run mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], }); From ad0b86c1eb961b7fefa9a942205d033157ab9b53 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 18:17:20 +0000 Subject: [PATCH 71/93] Fix playwright flaky tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- docs/playwright.md | 3 ++ .../forgot-password/forgot-password.spec.ts | 53 ++++++++++++------- playwright/e2e/login/login-consent.spec.ts | 6 +++ playwright/e2e/oidc/oidc-native.spec.ts | 17 ++++-- .../one-to-one-chat/one-to-one-chat.spec.ts | 4 +- .../e2e/share-dialog/share-dialog.spec.ts | 6 +-- .../e2e/sliding-sync/sliding-sync.spec.ts | 1 - 7 files changed, 61 insertions(+), 29 deletions(-) diff --git a/docs/playwright.md b/docs/playwright.md index 315033955b2..2c26b7ab2be 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -77,6 +77,9 @@ test.use({ ``` The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration. +Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as +they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId. +We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution. The logs from testcontainers will be attached to any reports output from Playwright. ## Writing Tests diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index 71475e892eb..af4e6def7ed 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -6,16 +6,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { expect, test } from "../../element-web-test"; +import { expect, test as base } from "../../element-web-test"; import { selectHomeserver } from "../utils"; import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { Credentials } from "../../plugins/homeserver"; -const username = "user1234"; -// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. -const password = "oETo7MPf0o"; const email = "user@nowhere.dummy"; +const test = base.extend<{ credentials: Pick }>({ + // eslint-disable-next-line no-empty-pattern + credentials: async ({}, use, testInfo) => { + await use({ + username: `user_${testInfo.testId}`, + // this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. + password: "oETo7MPf0o", + }); + }, +}); + test.use(emailHomeserver); test.use({ config: { @@ -45,31 +54,35 @@ test.describe("Forgot Password", () => { await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); }); - test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { - const user = await homeserver.registerUser(username, password); + test( + "renders email verification dialog properly", + { tag: "@screenshot" }, + async ({ page, homeserver, credentials }) => { + const user = await homeserver.registerUser(credentials.username, credentials.password); - await homeserver.setThreepid(user.userId, "email", email); + await homeserver.setThreepid(user.userId, "email", email); - await page.goto("/"); + await page.goto("/"); - await page.getByRole("link", { name: "Sign in" }).click(); - await selectHomeserver(page, homeserver.baseUrl); + await page.getByRole("link", { name: "Sign in" }).click(); + await selectHomeserver(page, homeserver.baseUrl); - await page.getByRole("button", { name: "Forgot password?" }).click(); + await page.getByRole("button", { name: "Forgot password?" }).click(); - await page.getByRole("textbox", { name: "Email address" }).fill(email); + await page.getByRole("textbox", { name: "Email address" }).fill(email); - await page.getByRole("button", { name: "Send email" }).click(); + await page.getByRole("button", { name: "Send email" }).click(); - await page.getByRole("button", { name: "Next" }).click(); + await page.getByRole("button", { name: "Next" }).click(); - await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password); - await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password); + await page.getByRole("textbox", { name: "New Password", exact: true }).fill(credentials.password); + await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(credentials.password); - await page.getByRole("button", { name: "Reset password" }).click(); + await page.getByRole("button", { name: "Reset password" }).click(); - await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport(); + await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport(); - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png"); - }); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png"); + }, + ); }); diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index ab70e1d1869..d7d5861a02e 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -77,6 +77,9 @@ async function login(page: Page, homeserver: HomeserverInstance, credentials: Cr await page.getByRole("button", { name: "Sign in" }).click(); } +// This test suite uses the same userId for all tests in the suite +// due to DEVICE_SIGNING_KEYS_BODY being specific to that userId, +// so we restart the Synapse container to make it forget everything. test.use(consentHomeserver); test.use({ config: { @@ -97,6 +100,9 @@ test.use({ ...credentials, displayName, }); + + // Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts + await homeserver.restart(); }, }); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 63cf0a5b59f..a50730ce747 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -17,7 +17,15 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { test.skip(isDendrite, "does not yet support MAS"); test.slow(); // trace recording takes a while here - test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => { + test("can register the oauth2 client and an account", async ({ + context, + page, + homeserver, + mailhogClient, + mas, + }, testInfo) => { + await page.clock.install(); + const tokenUri = `${mas.baseUrl}/oauth2/token`; const tokenApiPromise = page.waitForRequest( (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", @@ -25,11 +33,14 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); + + const userId = `alice_${testInfo.testId}`; + await registerAccountMas(page, mailhogClient, userId, "alice@email.com", "Pa$sW0rD!"); // Eventually, we should end up at the home screen. await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); - await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible(); + await expect(page.getByRole("heading", { name: `Welcome ${userId}`, exact: true })).toBeVisible(); + await page.clock.runFor(20000); // run the timer so we see the token request const tokenApiRequest = await tokenApiPromise; expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code"); diff --git a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts index deefb305dbc..8a4401f5f23 100644 --- a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts +++ b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts @@ -17,8 +17,8 @@ const test = base.extend<{ test.describe("1:1 chat room", () => { test.use({ displayName: "Jeff", - user2: async ({ homeserver }, use) => { - const credentials = await homeserver.registerUser("user1234", "p4s5W0rD", "Timmy"); + user2: async ({ homeserver }, use, testInfo) => { + const credentials = await homeserver.registerUser(`user2_${testInfo.testId}`, "p4s5W0rD", "Timmy"); await use(credentials); }, }); diff --git a/playwright/e2e/share-dialog/share-dialog.spec.ts b/playwright/e2e/share-dialog/share-dialog.spec.ts index d5424a681d6..58574a46ffe 100644 --- a/playwright/e2e/share-dialog/share-dialog.spec.ts +++ b/playwright/e2e/share-dialog/share-dialog.spec.ts @@ -23,7 +23,7 @@ test.describe("Share dialog", () => { const dialog = page.getByRole("dialog", { name: "Share room" }); await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible(); - expect(dialog).toMatchScreenshot("share-dialog-room.png", { + await expect(dialog).toMatchScreenshot("share-dialog-room.png", { // QRCode and url changes at every run mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], }); @@ -40,7 +40,7 @@ test.describe("Share dialog", () => { const dialog = page.getByRole("dialog", { name: "Share User" }); await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible(); - expect(dialog).toMatchScreenshot("share-dialog-user.png", { + await expect(dialog).toMatchScreenshot("share-dialog-user.png", { // QRCode changes at every run mask: [page.locator(".mx_QRCode")], }); @@ -57,7 +57,7 @@ test.describe("Share dialog", () => { const dialog = page.getByRole("dialog", { name: "Share Room Message" }); await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); - expect(dialog).toMatchScreenshot("share-dialog-event.png", { + await expect(dialog).toMatchScreenshot("share-dialog-event.png", { // QRCode and url changes at every run mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], }); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 29a612ccd32..1ab7909a478 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -108,7 +108,6 @@ test.describe("Sliding Sync", () => { await page.getByRole("menuitemradio", { name: "A-Z" }).dispatchEvent("click"); await expect(page.locator(".mx_StyledRadioButton_checked").getByText("A-Z")).toBeVisible(); - await page.pause(); await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page); }); From 6a62ae50e80857b78947bbb1c9a8ec301bfe4754 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 18:40:14 +0000 Subject: [PATCH 72/93] Wipe mailhog between test runs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/services.ts | 25 +++++++++++------------ playwright/testcontainers/mailhog.ts | 30 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 playwright/testcontainers/mailhog.ts diff --git a/playwright/services.ts b/playwright/services.ts index b480cbc4054..2a4d6ddf829 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -14,15 +14,18 @@ import { SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse import { ContainerLogger } from "./testcontainers/utils.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts"; +import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailhog.ts"; + +interface TestFixtures { + mailhogClient: mailhog.API; +} export interface Services { logger: ContainerLogger; network: StartedNetwork; postgres: StartedPostgreSqlContainer; - - mailhog: StartedTestContainer; - mailhogClient: mailhog.API; + mailhog: StartedMailhogContainer; synapseConfigOptions: SynapseConfigOptions; _homeserver: HomeserverContainer; @@ -30,7 +33,7 @@ export interface Services { mas?: StartedMatrixAuthenticationServiceContainer; } -export const test = base.extend<{}, Services>({ +export const test = base.extend({ logger: [ // eslint-disable-next-line no-empty-pattern async ({}, use) => { @@ -79,24 +82,20 @@ export const test = base.extend<{}, Services>({ mailhog: [ async ({ logger, network }, use) => { - const container = await new GenericContainer("mailhog/mailhog:latest") + const container = await new MailhogContainer() .withNetwork(network) .withNetworkAliases("mailhog") - .withExposedPorts(8025) .withLogConsumer(logger.getConsumer("mailhog")) - .withWaitStrategy(Wait.forListeningPorts()) .start(); await use(container); await container.stop(); }, { scope: "worker" }, ], - mailhogClient: [ - async ({ mailhog: container }, use) => { - await use(mailhog({ host: container.getHost(), port: container.getMappedPort(8025) })); - }, - { scope: "worker" }, - ], + mailhogClient: async ({ mailhog: container }, use) => { + await use(container.client); + await container.client.deleteAll(); + }, synapseConfigOptions: [{}, { option: true, scope: "worker" }], _homeserver: [ diff --git a/playwright/testcontainers/mailhog.ts b/playwright/testcontainers/mailhog.ts new file mode 100644 index 00000000000..c3305607d89 --- /dev/null +++ b/playwright/testcontainers/mailhog.ts @@ -0,0 +1,30 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import mailhog from "mailhog"; + +export class MailhogContainer extends GenericContainer { + constructor() { + super("mailhog/mailhog:latest"); + + this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts()); + } + + public override async start(): Promise { + return new StartedMailhogContainer(await super.start()); + } +} + +export class StartedMailhogContainer extends AbstractStartedContainer { + public readonly client: mailhog.API; + + constructor(container: StartedTestContainer) { + super(container); + this.client = mailhog({ host: container.getHost(), port: container.getMappedPort(8025) }); + } +} From f03ad7b8abac65b491d612c2dbb8575344cbfc7f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 18:42:42 +0000 Subject: [PATCH 73/93] Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/services.ts b/playwright/services.ts index 2a4d6ddf829..5e2679953e7 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { test as base } from "@playwright/test"; import mailhog from "mailhog"; -import { GenericContainer, Network, StartedNetwork, StartedTestContainer, Wait } from "testcontainers"; +import { Network, StartedNetwork } from "testcontainers"; import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql"; import { SynapseConfigOptions, SynapseContainer } from "./testcontainers/synapse.ts"; From 6e36a1ff9cbfa9fc605d27d55bf865f2fdea00c1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Jan 2025 19:02:03 +0000 Subject: [PATCH 74/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/oidc/index.ts | 2 +- .../share-dialog-event-linux.png | Bin 19318 -> 19219 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 7e9b03ee6ac..bfd49b496a0 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -33,7 +33,7 @@ export async function registerAccountMas( expect(messages.items).toHaveLength(1); }).toPass(); expect(messages.items[0].to).toEqual(`${username} <${email}>`); - const [code] = messages.items[0].text.match(/(\d{6})/); + const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/); await page.getByRole("textbox", { name: "6-digit code" }).fill(code); await page.getByRole("button", { name: "Continue" }).click(); diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png index 541eaa2fa55a29e032a6cfe94d351bfceb572d2d..6a55618c78dcb7451978fe10db216f9acdec0f5a 100644 GIT binary patch literal 19219 zcmeFZWmH>jv@S}8@=@H2mtX}76n7{P+=@FCm*8%J(iXSk4#nLi5WG;_-JRg>mYjTh zkF)Qe`*WXt#~tI0kz{48cdb|En)8{@obOD+z9`FJy(N2#f`Wo2Cks?VL3!~T`QH2I zCGyNS<8mD2;f1T3j08&MD8)Vs%6k+!pty!t#?gwm0f8r6?95Z*&ucpIXy8sQeru=J zZ>n*7qUE<0ns%CQhcz>%5Ix_;27M@W#`bSR$D;e;jHWO%v`nLesN$_129^F7ylVL` z3U-O#?Jk|zto5NGKAQFeEVvpy%s9j`2uC`Q{?ys^=k?g zr>HMq?R(D0mGYSyKW%k3{-h-pcw3-16xTMbK!wZAV0+WL-!=IV%N!3w?eLDHk5gQ# z)ZuHC!ElfqB=06>ut~$QpgS>gL5%m8Tv{nzxt^pBo~YkU{vttCls+m5+PFUSh7!-c zXLs=1hJmgFq#gRhV&dG?W6zw5c~Vcb_Q$`2fJO7X0gCgH7Uv;jA2jrm=uNL}E&Lz& zj9LAi6@f>wf3n!B)vMFgEJg=@@|*%i#T-4UmZo-=#je8winJT=gQS zxH&o(zfTk=Hj}1GkpZ;aId2*Y{Kfo;AO5Mt$BSj);*b&%&|9>tW;1iFElreLhX6lR zOLoo-EY(b;Ib^Al-&96ils-;_E;iRv+cxvluEIr(={ssHT==MF_d^9~{N7a!oK~cr z&VBmL>3e)6=AW2Hqei*lb@yZ3mvfV*?+z!C<&vmely*bb$m{NHX znxyCoW`!~6WDK`t{L_e%^N9$plQAs_H%{Xxa9_wJV>H;Mcv*&xnpe!xb}u%k~35D`W#)+FL5r; zUByLWV*voBt~U%s+~AT6^G$SCyjVZQV(lSyZ5ZW3lb26BqvZPhz@=!U3ez+Xr9zQngfSTx*ss4j=OQpr zd}AwbmwhEO$Cj5EPgM?fvUABGLbNCLc}aLwGZvu32_$f?1y8sK75_nP#dByKhS%~(%fepwOfW*xEQwN z(s>UY-QqAG+$w67scn}MHV>>(gsxWKM)X@&F!DN1ecxGfO+V_VXQD=&XWj+(h8k52 zt7yZD0_ylKwphun9z9zV`}*_o0jzj7I?qN+p{;p5X**$I1p|9qvY->%Qw)Y(5@t9W>%IjnTD=sdQ8546~&_!v~72BKT$H^H>&xps{o+W!0rW-y1|UDrIv)DyYe|060_A|v&kBKdd!;+o{+85f-$hT#^X@yz^_;DUtr?l zmBg@TLHE*om&D741xkhmQWLoO;D;7Dqd*|^`>P0J{j_|$ryHA@K@ceu+70`1?6`IJ z!A0Zn{gxH!weg42r|l7px>3;_M+Ho%rh9+pTlG?H~N`^XQYy1UBP7KYJ_>=F?M7TvOmdGR88Z!F($dbT`a@2;{a;IVOtX2JZwGFS#tdMDL2J-$w3Ty6mW`T1|1>iO*@s5h!<<`{T6+H)a zrbZ1g(s!?%-Z{I<5aQ#I(2)|#Eg(eF);za($F|V2oaV6kTUUY4zm4(y@Op?fvCMUA z)i~QKRsW(^M*%A5mnUk_;=7jBQ=wpmrn`Hynx;sJ7Yl$ZfuM3fj6$FNo*OF;w}_)- zxq$g&3s1@h(uTl&&g2f=WC#4QYTc?ne||o}9`*QI7w>0JNg3_e!(Vo9&LO>b_dY&z z&zIz)47MjNWw#VU#yV4O!{L&y;EKE`J+IU8-S;!97O+F;Q9=H$h9r6JeL#m1(r{jAgVti?Tk0qBiD_Mg_B?H8o;I%y}zs=M>P9SWS? zaHOo$a5sD8KGgDot<=XoaDdpJ!KN!`9hq~wz2)Sbwj1YzP&|e9C{l-&KPCYp#vRg- zj#P`1*~bN}WIgYYQG|QFJAplX!JwLEk`sRBIpyZQ)aaAQYt8tQUzmi`?Rr^uo zbgG}2nQv|zOvaOcK3gmRhdN>`HEz%Mxsv$b4a`38^Vtqt*4_8T$mqhv{LRGk_&z>Q zrOd58dtJ`ipz>JOH3LqU?6aSCqMD~x=U2}dQbv-s#6YPYS0SXcSk;dfhob~%7jt5B z6{=gAgG-*X@aT$vexuHPCcYorHaj$9#7flDGcDIjb>(!vti5NC#=_2q2$hCxVYvyD+>E3Q3&s)hGPsHY?cD=_{fa?E$AW(x^lJ4(i=K@R8PUU%TCv3Gys5Ri9!I z*Jd)FlU4G`EJ@kXZF1G|X3|;!2^A^~7PUZ)6c@U5dVpy!TPPNY-vb}D^>TvS698bA zS01!c`)2NTHUHpXJ-PlxVJV_jg5k|vRqV%48V-g#66s26Nuk@Z6OyC=fW3=psl}^p zda8btu+UaOVQ~1#{)>3e_qyW1o}b($89;c1B^n11SU|e}7Jx6;ljNdnVXI~Y=9%10 zI-Mtk*!f@SIO5^O{;a7?V1EAG-7K(MLv6ow@%IAN_0Ki{;Bu2qQD-;OOBqJK%L5&j zjEteCgP##x6gJ=ZFtyX(Y*3G-*lDU{%S}<@V}VI6S{AJWu1j z&l7n96uzm_P3($;^~7EchQ((R?K6H{S@X-w{SlgGOkSd4P)A`|mpb$voYcUtl2|G$ zBcA&^bpnpY#}qovm@GzJKMJyT8vlY=|26)LBj?5BgC=@t7Ae$9GdP$#)mSQKeq)E4 z5SN>q+sn+>%*P9mt6rf^ zeXO9_6dJXIV2Rx%b4{{*vG-KPIR%-u&XU#qygd1e`|&g52<9aK5Vd*Mj1R!YS_tmunUi!}f@Z8kp-Ky6S19Kj(j&h9_{%>9M2*E#rcX2g@;K6pIt&reU&*^CJtWpb6Yccf%{yY$JAzD(MY(W`m(eVBBS z^js;OP1HAy%f?DQ^YbyJ_gA+`x{qBWH!)NY<{$LDRzuMI68AZl!z78cIA2jAG9~aG z2QV_9ZI*e4ESCXwe_HRb7|YC^$!7bgCXS$lY|=7+H=$UCSqA_>?1i!Vqs>sO$`{#> z=}=B?Qb^@Ot4J2+rIvbo6F#`OxYTiR))6(63P_P9afhFpB8n~i=Q#|ef!h?qj#KB} ziKwZh0?nkP&2ObF8zqL~j8ZQa?0D#@_12Riys(DMw@INra6AG=nczF$`-l4>dPZWM zlbbU2wG4|>Zw#Fz`wOhu8lrpqZUKG_&B=5;lP$oX-l!O*=G(*A5=YJbdF+*?e0gPI zGl3txzTL4un{Z%2sh@9#EC7=>ivrZ%-;zb}@X7|Y;$(a3&#GuqV{eD&b+drLe5D6d zg~sdpT;Zk@fTeU{?f&cVdc`4n;hJm%WuX5Hcv4i(F#pt*EmL~DiAV(9_3d$%(c4io z<3_hSRjWdV!rf|MPh^&8$&TZDtLCa5)ToOz=wH?aVM3kQVKHtOSnduYaxH-KG30u7 zsv=GC&|4LQ^6-e{!r`D2tZ1?ZuDDtWZ_DpP&=T*@E?8*Z>L{7W>qRimt7~Vy`+7_bgV00e|-Gu zIy+vC@Bx%o96p}k*iIKR3CI0f7^S+2t1iY>=m_6d61pHWhJBdFdp1WBZ?3gag?-s< z6o$JfPXKGfhudCoo-LG6t*2^n&DsMMy$qiDbM0>Qx5yh9&s{Z*lvocYxFKd*LTR95DT>uQF@da6`g zr>Q4|eEBk3X)`%FIErlkc%z2g>%o)pI&Qr3s-pk&1TgSCPTJgtqhPC6d|&foyf{M} zumxz^9$n_~y+K9wYUavk)!5$tP|+6`_sjcIQs`>sL-Nqy{BeB4r8_wO0J-|Be7i6J zkI7WWlzywwZ~9s}1!8*h-km+3)NCoS00u!HziN^^)y#Qngkf}=EI9aklG~9o%fj?u z$0J$e_^~)qv#G^;IRvIB!)`Hs#1>cb?w%ajtz5|E3=p_keJkoU z>O$G=Yk5@OHnCJyqmgj82%M=eiG88gxV{Z{RMx8$^-_BHs_F!3c%L%97j_{clxS{) zAJ(1Q2k~#Fx1I>uiZ@cwRJYkoC%|A(%A{gW{>30NJ)2Efil^dX!Z zxX)ra%BF)$31c!*Ad=JPwM0hG=5FSpKSN_VN+}AGJ_M{#7ZLXkW5&0{Gl+@!N9>@p zMf#cMQ_Fqd8O~Bm`mhy&O3hUz#+baPaX)Z=NZ^td(2c67kK4Te!#t{h(bDGXD6C6O z&5B~)gI{~+{4X^ohDKe@cr#k21On*C9oi zgs>-}4*Li(o0#N=%U&BUXQ6I5-Vxl7twGzzT^N4|irKjOt99>fjtA-Ujyg<}Ti!>z zr=>=8Fqtk)Mv53)a93I#P2P<$dVts;)>EgRJ$rQprDXfK4`L=?>TWOoj3MbM7sxYJ zaBaV5-c6UTk_+y&(*cRp5jVT6lnqKPRoTqvUL6ljLios{&ZFRd#}tkLQ;9Ww>arA) z58Mt6s&M~tmQOHN-C*$3D(Ltk!fi8DDT zhLKRQd6;IVoQ`WRr=(5K;7(ja%^J6W+0rNxGQq&>gl+WeOmgsK#x+kO*hlV|6kvKp zZ`7WHr)~I=fwDzjH}@)M-W?|FHguZLo3 z+SSnU78LTc#LbF&9QZR@?F-ky2F$L}I#1HScUX*S_UoSMXA8=!+5S#J3E9`i1p%ZqO5?6v{p8r=fi?J+Tg*08%#V}=UeJWiKF1|ftJ%|m9n9MyxfYzO(Kq|*?3jD z7aYFv9frn;=ueUJLt>u}W)j2tu*QDQ7gV=u#uFywXNVc^) ztOf3voZJ#KVmyAl*U4-9JH+R*@z8_C^{i(d)!-`Se|XlZ@GXQQz43$7sEJ4{csqFc z?gNFd2a28(3J!WBosfm+a$MAi^0-!(Y;9Kc<=Yz>s5|Cp*9&1nw*^q1@p1`W?lI$3 z(MSaLWg=(3YL=XQwP=Srr-zXS+SrR%*s3q6@Nb1*l;DLShR8dNfD2v)|G)h*aTStI^wumk6#jwbDOBNy@$dg5!~TD=*8e-` z|4~)?zvBJ>k$C^Z?qNu~N74IE{NDvUFE_8<&{0qfU$Or81J!?Nm%?Y{v|ph72K`3; zpM%N&XY_F-N|uK6>|{Uva{C~*J8Kv<68k79mmB;BEX+12xjD%?^8y7AZ_KV&Wyw>g zrqFL1rYV(nfvvQ|v=8>!;54WB4C$gdc-A?WRFj#C3JvEbqAs91N()E%jrvd1qcgHk zID@eNwW-zIOOy(U&Xt&@WB${d&jhm>OReSuwLgb~ExWM>UxAXrwZ{g!>FOY5aI^Wq zf#lq2RqtrQ^6fwz{nF@++ofqlbfsbK_K>>0BjdX0YlFFqxF!nUowf~dn~;q^AhoP4 zB0|#2fLx7gFLrM~MJ89)%Zp|m`o)-W0OwN7%VB&gD{eKDOD1QR>vGK(bnr=TcNqKI z7IGV^OqTL*5&SAPJN>Bb6b`PRQf_0q1=rFKnLRLG$_Nrt8a~EwJ5PV_Jd`W(rssKC zGZxBJ$ADv#1?EY{@oQ#LXUvAfo?U##UonXjJEGzxu3||{H05BtO!oT3&${YH`tduDTV{tjAp0oaOwU=~&)bAPg)66p@r`Q64G` z;0(x@)F*!{_=dHKZR244a64P{$~FEkL%?MWwR($#_+J}2ZQ(yf9h_!<0ZCR z*!hfs7}P?$aF&r7mKf|t3phVx;@A)cZsT3Z4?m&1&?IIqUGHQ~{oUM9<+7BDa$+45 zY%*CYiL+GTG7xRD2(WBE+9&0}uv9vmfStsM6?5aNjue2&Ju;bzak4*|No#lotQ7pv zKgon1EmyMNnkNlpWpN@;hbrj8ve-R!4=S4L9lI;J3%)w_B#^4o3fsEGH})XDOv}fz zj*7cF%q2bR(r_pt4K~^SbO`FrRTqL3BzpfG%SJ?6O`Qhh^tkqa#c^K5s)_LnT@KuP zn3!e}+@HdLsoW6aqrMrGGWPTMNWbIKe?${iI9;kxPe(EkAHy?8Sl%d>`3rgvhZQhF*izb4!4!nNuwu;1X zdrt;v^)OLL;&&Tt`mCKxQ~5VpRDq@4NZCdAu;8uU!b^(^%w(Cidxua}*gi8|F3l#n zH#uk3+W4=cZ6Cb@p3I@C%UzWdb~-ZBJ@IV&f$4?$FSW`mp8f>5PB?TuGVMEmwHYw| zO%GP1q~pUv=^{T|&(L>2db2BuG(Gfq8XA7nIWWa~k5)4u7%6SG!m^_)1le;F%r4d? zcvupKPAq?5V|14N3dSCMgOfysoSBxgBpJBW;FZM7$s6NPW5Xlb(^Fb%;g9b-6GJio z&dG;H+8}h|X1xk`cHSJUx#9I*{BTwv^F$>q8S4Mtq9yUY>%|2$NFg{fT{u4xpr0z~ zEP(!!4ECP<=y1}uGpuLU%qs9(C=)S`@1j}k zq40>IupVjJSeSH8QV&h{!DyE|9U9Zc#_$i&hJ4IyaUEAT4;G15fEfYg-~g=6I37 zek7-j<@`N{tpx_Av$c0qUt%U=K3+z&I|L1r3hx6EjB+`CrurZ8_(bAHv;A>Zeze=) zqH+okN7FUMIpUA->QCOQk9loiF(t95^V_u>H^a?NU4^8NUV3`*5XU(j`VpaNo~?r| z8ioqas$z%F3p@pBK&o~nG0ouG!{jRKg1|AwERNfXnAM02j#AgU%J}i_?jYGR`qV^> zM51KU*Gim2Gu@JkJUfCR(AXXx`o9)*gmL5D+S0us??EVlCpqRs$nC`EvYp0$So~l^ zqL&=Y^+U5uMK@nsy_I-{eV|q_d8PS|&0kte))W!%WiHx~0jXf9JHCP;P{UtRHz2Ea zZ_zz-q(Cwx2Xzizls{`;fBSB+zXn(j>+w`jM^vd>f;4t5EV%uYDTQCYe1r&y6TFQ5 zyf+?jv5d~wt|-{ZRf5o0-%+Zv4L9ro{?)bF{rTde6TYzpcEu??UbOs&dLldAbxL^! z_YNq9Y4cFRJf^UisIiI1Nh93(x48Y6rs7NL=E(l*{m^gaB1t7XrrlA)W;WW^z~MO3 zIO(aO?!HGDl`z!4h}Z9D%opFC>4{OMmL`6IavI8L0h(WS54 zP!Zy0ue}!V3KrfQS*JG>7gFCf2JrzrhK- z87!G@g=!Ukp`tp7O-_QkG6u!`QkB!vOVax4y5HY~U8mpf=dCh#wx@gEont8(ZP5O^ ziicrmcTxDX@#!|+p1o5iBQeQSrfkr0TRR>xZ<62%DAS?9r8;7ohCg)-8y~+BB@pby zH`wuJtPTIG@+8_+b>6!WuYDz&mqj|6?sH9jpqCM!>RJONCY(>UsgE=QTl8)|J(XXQ zI5xkDq2m1DloF|F2>q3s>b^sKjpgq!8o722x(Pd&2`ElJznQ_XQnAV!>M+_8<~EhS zIu#}b6gqZpQ59}JMOcklW{q`pUs3byBz_18Gc?qEvC)+!fp*&2#kQE+ieWXD)mB{Y z@+(9o`?58g7LTJgC&o9$fykhLaB#3ibxWAvuXhOVIW;NV&H@{IwR_vN;$To^VfWG@ zY27_v%&a>jMx8a?))&<ww zW+N}58(|iMiUy9wZOS;4^UHclRDoej3RMtn;9mP* zIma!p9T$T$oN!*^;*nNTKbsCQkqL|X{R~N|pQ}~7bld!f=aR3#`!C5Oc_UWy{Ivg= za&|AS;wCO|wug5^E#?_a!*uL6$yTBK9)IW|WH>zS(@|Es*m~DGvVTI*V92wpuD6nV zNTG|fQ|r}@yzm+R@1^(aC+anYPmYM^n-&(5#ryt?-8UlV`*+E|98gx!|0A~$-Tr?j zoA6&W`Ts42@%ok{D%ygJYb-;^v%R=@4_2|n=eXMHe(2%u?oN_`hSDJw9UcAraDBW4 zaqMUZ3yX-zMEh~lnb(J!2nuFnKh=VrozG)WqYJY6y0WDlR0HuqPFMq~{7yq<2`eDWG@T~QCr1&k_lGxsHJwhVv#LY3?R;Bm^BpS}a6MgZTS{zr zfg)UdadAP)J_d)S|6pxjl;}KOZDYKt3gAjrG&Ckh>HNX@4)c`FsMhy3%h%Y}ANypv zxqOeb4(V?BlF(4je!qU%1xFl_^4J2m0+n7?HZ|QfqsI>o4*KK@qkKc#JUH-q+M~h2 ziFu&G3MtmB@Hi<8kbu7Eq%AEkw=)_(6wIvjmzC^%1|50?0Xjn7zCzi1?d=^NmEK;8 znDPnLQ_H0y7d0xIxv{oKLBWv+;8{JL=GWEb=b%EIr-+g3bNVKox6))2v4aq!+vFK1^}(7g;11 zOPtP|KX<6v+kCEfldYc5Z;tT+4=&v>S~)cB^1G$GQ1s)JK;P|MG+t@nFj@7KqAzh8 zY}7)GY;+&*GK+83XI83T5|_Smu8Dik%Of{sAMsO0(ZhqS<4eK8KId=!iZFfmoxvHK zDJ#9nsL)D-=Cq7BLF?byZEeSv(QcE2?h{72alr?gH)lTtl3b)7i4kSSPI7tk3hI3pLD zqiDbnkE&8f3K4^?kgn2Sd=yvCSh=-S!*GSlFyJ?NJ@<34+I2@_PQ~-^KU@~Kngs`& z53+?P2(gb*X^p&03OUf6e$hsMk6(AIVfkj-g~pntwKqL?TcL@M8bQqdVbI5D=HF;a z*`miJJ3Hb(-VhUhU}51Y(_)Kxyox_Ns{zs_Kem6`Xy&pS+rMt~!Qg;*!7*buz2sH5 zx&|FZL$f$Q6ImeOHV;T)6PmhVly=mO7U#lkWOB)StWSMB^T}o0m!+uA3cvAm$8YeGl#YC!?s_z_}=uaZcE^! zDn%yX6L9iOrn{x0#Rd-D~$15$j0@oKC*iay8@atr+7;3#h$~lfDLCnn8Uzbd0vd4QUpfT*x0D{jiO9oHX4w-6T>I7PuBgSv@v-1pu=z}kt}S%0g| z@&3UV8_sqVDMIm65PY(uGlw(OBBP2Df;%WA1-%Y85F%7&ep|~XuywG_m+s&t{-Us zy3}?{-GnPx&)#35=v^DS3uJNc&k>;-ArDJ)Lw4hxEYyjJ+rpCkoOny=AX$xlq1BXg zWz)Pmn?Vr;h|P9vJYYMLnbu_BDCVH9@B`C1kRDmc#>4^Px*YjF5)%-xbB=!zZPenB zW>qvsMa9cml5l&RlP~4izBHBinZ#!^tPklIw5sN)MLb#yrTJ_&Uo$cm1(YK$5MX$W zLnYnbRzf$&#Zl9_YPx=VKya8&c8z^5ov+1wgwTW-0o8af76SKYNDz9E#B%hYiP*E7 zGwev4*ZyfYB0ev)xUdj*>`6@h)FCNisI3lHPwI=r`iGRA_Buxi?U%#Zx1SOlr+q;f zbol4$IW&yQSqJ+*zcDg}y?y4lxTTwbxRf2-~1q9BK z#bZ0kS^wcNl?iV58C_w>l8=eIP%B^PJERjOBqcQg=Q^&5f>(W!RR@)5D8F4>Z%>vu zQ|mXZUh$9}r&O;cC#~-9|3N{a3QbR6+K*q0_`4aLjg=tV6S1l@KNa*Z58)$Wt-k9 z5Ar~xM3uaKazfnUUibO^Uf3cE%1L&piAPVs4F5_R8U>yR{H{8YWthA8gKNN+BhgY)W zpsb4ym`~ax7AYtwrZ>wf-caBX5tqUcHYP?{xhzAkP?}WWk|9TMADL{?+F5C9mi3P+ z1WPrShXMZ0HMRI-Xs^G81l84Tk4Ryd8D%cr#N03b!ygeoICy>7vE0n^LPhiCw=85tFR-Ln=)zML283*l7T|yF)fZLVxD*{F&Pc_kfDyKtnqYkd27V*)TpXZ2YXdAC` zT=P(#3H(-8WVSFo%(Xk3$3Ux+MG82yU>l%Ct2_3|8D?TkP z*B+k7BfIBz%BGyD@d&k{eCGMA0_Q(qo!oh+jA@P?$Rx`L?wrupLby*e>%kL%mL7lF~69#K1K z@d#?&gYeF+=fAEgmsVOJ=K_JLmN#2D^R7oWG+4P3zC!a-Sn=`Pv{N`qUm2Ip_#RM_ z9CpuF`()ylmk0C|HdQsv&og+wyUa?G5g)*XlzsiOo#}2G<5JWNDwg>E<_~A z6`TH(^eIJQ92YXtZC;5V+g5G3e|npuJz7x4>6u(vnOeb92Kvx2?p4|2l{2j2H)qZp zUzCDqViD7^iTVae)Yk9gBS?drgMcd{pcDQ@< z^wox%F&IzSU@3K;jJa8SH(FI1|Jg6Alm`fm#*af&?FYV11*$pX3y5m)kSoH3-b{Hb z9hbcA9@Feix2s6)|4|b!v?y{6prFTVdV7E4x8x}#{B;C}CYmC*)FDYj^YG0O*e=?J zH=U?MBj1wP^2x2GUSe*O-(fUYwcC6w`hr5Vzm4*?bA0Ffp($fu^y=eeU|QJdYNg0s z#Kxn9nVizlY4UI&35?0h=G%IS|M#1tenUepyl1aAKTi+UxXl|SR$s>n;aq=H%Rjga z8DGb&s9ZN1%p^U%N2FgF$`&P1CL}5@)+Y7gwWzN1hi52sZOOeqI!{xwbT!gEu2Fj) z;A~s`>TtgGv2Isv2avHoD!f5wtunY`Jc7g6vNM!?>xPy2es^+aU@ti`+T5i?X7v+fRJSOfFXZAx%qWYHl10)gnNpCr?}@E;GvPcjYQybn*3}e}$;J zO>wDxLxGwmsNF!*w{db@;j@2qBi9Ms)}P;*X5AradE?R>oX>kwTE>$az^`8qUn5~T zJ!uD}r^Fxoz6lEVWk;jRd(&;0o`1fy`Ap zANS(w5kyL=e-Arc32A*ZF#^(XP7fAQdg(i;OrMwK!AAch8jibkQ)5eVOg`Xw6@X}v zpL1LL_13$)=es}VTI^VhKB#>l?c#(?;Sm3eHVD2c&66z?Z?3p|LXN|#{MtZQsuP!- zq!DR8MzcHnmEa!HWUZ?COQ4dZ%KKiumreeH) z^|PO&XP?G>wp+rMnX26O?YG8zbwJ7^A6wmvt_fIxZ4|G~zCY+qj+pam&%9%bGP_=H=dhvE@-nzu705tyvUuf0% z4S%nfz_vCiBB3ZXPG4Z_I|FmeMR%EJStM-!MKnSF&9$or`LK+_wV=AbaRz1Sy~#~K zWSzM|lb79Qtz_H+iHyh2N2pJ5D*PctK)dSw^#%)M&^ENVDL*{a|7rgwV&j#c2t6{t z&){A$t<`+v0Lr+TE3eEs#UQ^JFz4+UH%`$qx2xBV|3UdI@aw)Uxl{b}?o0!sMHkeU z_oL(830Y`pJ3Du~U=Mebr~aBrrap50NA20+Bfyr?vO&<*k7eIXmdSj$!Q#!ijR@Dj z;Hs}80$b}VS+XsRBX<)msK9%Dv%RGP@uvqspIIv4uh5T}PTjiy(H+Q#%KwmY(;xA~ zEL7jYI#$BVh6{I^{K{J=u$UQed4-4WwGCLui}Bp3JYQj1ynVd&DeBl_opn<-Tgg*S zJj{FKY{$0eIa@Nm>^_RcX(~M+W0VPL>>Fq=cc_q&6`_jR?blg5jX?FRy_o5hK1R4; zr5AjGP&&O7?P!HoNSlV(D%&s80y~R%a)vuD?(~U#z=mZf^u`-3l0R=egaK*m{TR1| zPro^{)aRH76@{r1r&Bt#NzrZ{4!cY;4KBwDkR}f>jcN!J_-nq8Y1+p~6{E;dYo|gL z+ZkQ6C33hsTSbyLS!**WWM>H4EKGWK6t`J=G1ts9)ohmEHZ}WC4C?r?aaaWdpp<7a z=n6(_J@i4S;P~6&a#|g?w0j-~2K!)&mBG>6vmO|_{C<7H)!`=IA~NEa3Q#Z4+t>LT zz66aO{eEwRy-TDw)SkziQpeNV_^R@mM{Jnt{@c_cRubblghnC9VXXW>#gRM;=DTz% zkgQM?lMLZ#ZfhIAFr(&eD1y1tD`B%^G1Tr(#Jzv~>`zqy9Oz zb&aoP)Xdbetg-fWPz&PTG`G)FWWd^B*C_Vd8~(WS9#C4$+uFo(*mHfs&VUK#LzW8j z4VD=zJlRZ5_w6{eeN-m`xpCc?BTjvSQu`GX3R^F4PN@zX)0k*WHW0$13laSq3c^$Z z3QT*tKAWUu+;J3{`(sN?>_0{s)uqr=73pyC^(A4@sx;TW{hb(S1#D_3_yO+FhT4k6 z57ybMXUaCp7^7R4lpPwVKB%&U=a%>aOyR}$629-!CU13c`jye_4x$sklcsMOWTKbd zm_<1WPouVs@$V#!ypbRW#UemubMWIkVG?vnVB23TR!X$k&J|sA&h!yW*B~up5GTRJ z40)nGdr%EY?eTKhlImqpErZqCPvO<9m)+E8OEM4tbPJ;j94s0)6q-1UbOb^SOvA{c z(by9g{%IPmxL3?^7lKT98yxi5r9SkaK!RCtsPHF>Vt-g2jWoM!s{nT+pzcCbW4jfI zjU`oU!l+E~8NJ%NykBA4%f&^Y9^vp5nMId}bA3)S*&~U4L{4P42IxIN4d!7rl1zRb zLII<5BDx?5cb{QMThQiSl62dzJRc&T>Ci}ri}vr!S#2bi>T$(}S0oB83Sf9a=AqY> zC-vYhlbby>1EKcwlX`zYW>P!GxHn^ocIF$Lc zwqm3w*R`V{;*=M?Nv}>|QsL|VZP)y{#KT*Oll@@tvc;aX;&UuZNSVDsAkwcm+S=Oz z0Ql&4G0lR6Hf}pJ!8x3a3cIVg)Fe0@X6Q}&jh2%GtW5{d%e}S?n_gqu+_eATdW^beC0Ahuk_prdB?uQ)XoSko=Im4n)qf`Il z)$f3e2VcFwoLTecKU;^ZWeyxG&`k2EL0X}lYcU2|@U>_g{Pm8Np7F?7KA+6wLy$SA z9!otz{pF@y@(Fp{k(UIO=DQIpKzrSh?3f$LpCH@FdYqcVA1a3RQDWtTafAh<0z_as=r zD)#zoZdH6#w)*~Z+Ca_8pNW;Xg0-oo%weIWr;|Z$%nzv=+T~THm0vb22A{J^Px0|; zX0BFf7Y$5SH&7hbO@j@}3^woR?6_Ao|G4AYVw+MY6P zB5tFNmOiX4AKCle{3mXU0UIDBwLX$dF?v+U;6=`RXzqutTkn(r-v}`B7h?NMNhW1> z=v&9NHh*;xZ~Ug7H~6+!QK%aQUkY1!+6qPBnn9s+$11Ld zE$of(Y9Ai?o-Y0 z-%t-L=Xe4h7a8*G=bO2wPrlo)qVX0*)p5Cdjy05rLobf&QM{YeL<%-{ZnHZws!MWyZ4y1eC==&9Cx0dF}N+F~t(xvfy!7*S2rw*yk zJ<8Y&bx~6Vc_9PK8B(6i_HIESNk@Oa zF(fMi?HWDmwu&)xDccetm(*dHo%=D=;90=kzRoRP;0yq^YwbW1mCGLmt8G$zcJ2ap zl42Z}%kRlm_L17nWRCyLIQrm3w_>>15SerY@Kh;172o_wPZh%~A397Z8XsDG!~njw znp~;Sk0g)E7|-Mh;m5GJ@f#`#CKsH7&_n0iD0jWXH#K>xU^YkI=H8s$VJ_VzabihIuXalfEOSG%>=_`bxWzl6oZrhY2@gz>>&j%Y0TUmok^cMI< z9xI*;S!o5)t)Iq%y-Y#-0@PgxxUfvdqaAz@G*KgcbF|JeYdRA;^M@YsNhJ^xA9V|H z1)Xg5D5CHAt`!w}0||orsazY*a}Ji%@z4O4M2$ydG? zr#2)e5ivRF$E;Uh?dnhd(_|gHnww?)2icKvl)Z4!AEW*NkA&UkR{Zo^u6o%?g}Wnb zb~IJ}r9pGf21I+(&Ghiue!+|H1b=&%W-O^@O%s z1R0prYj+D5eJ2xp`N5B&`6_4i#Jo9VLz&}wO>D0vK-D78f2++$ecRwLtofvLA#2Q6 zoO!UEd)ZP6n9ns4{3new&)b&T@)@^g)SQhk<+X5Dx`>K4NK{+b8?rx_9q|kCU4zc{ zvZYG$KRH#M@cm~)EgDzQA$I%5k7X;RcTF|{SzXe`5wN4XRxx3|Pu8JP1WpIFZ%fLe z>b9bg?5gMiebs-t9queAOm$irI~h)Pqt&7%`bt8y?fLbkh8Ve{gZ8xW;F9+~e@7(w zv2Up!Eg*1Z(&?TnW&tV%!LTh{Kj(ILj_;$_EVV)CJ-`sL(;+@`p?t(@)B6d3y?<*vtrhYT3A2w+-X4a=~qWa{CTB!0P@pahs&-VWPJQtmc300 zSnpbLJ^C6WKFk3E?|&~uGjjJUH+kOA$y@JFh6)KN>8q_}h})oax$rn~hv=>6W11S(ciWU27>pCaAi`D-X@B8M?Y= zva=)qr)ba!sx{J`o2~vVgV>cqCeqxMH2xK`xBX4owcDA=*esj&rs7#drTTKb$!7t6D_#W7fss#|Mj;cJ*hdpjbksxk2Syk?K~ZB31i&CUBc^vPjeU2~0*Zrn9}uQFz8WBk&Cy=LK+gBC3zh<3@irL!p z8e?XKzM$GzqBom~`q=Y)9R#C#3r9N(``Pmy+LLV$R`WxMI{{ZxT3&Le2Y!_M;{R{& z?s^i0!T^q+PH||xX(-jomNt5s-sF`Zziw*tBI|~&6;9o#Sps5Zyja);n-?{M1QCwU z?*qX9f<0pgINa5D_za(!>o5oBS>=$(Xa;R+RAQXgcR-H&+mfhs5-`Y;;R9g zeS+p_1Z<8WdIlUqsCaVt(2xz&d()m!d>CfE^;kMC1UU#adxGZhy8aSMbKFoh;1EKU za;aJqqd8Z#CRWPD;KzaHzd>^tMc%?{jum>=;6_Z#zLQ#KrK72yH z@BISQe{@v6-g>N*i=OY?PXL-np!tU+HpcqnPT%Y;9TvF zrf9mN>gm+_HqT+az!Jg|hA@o#mkoO`ux$Iw89u+Ly|+HkrLvjL*?x5l&CpC!bHwIr zz>R47>+5^J-#09KVA%tA7kok(mM|<~R##Ux)>h)S+aEMX1Da_vrvnZEFb}=~|8`j% Tv;(^t00000NkvXXu0mjfV*GsD literal 19318 zcmeFZbx>SU6EBE_5Fn5M0fGm2cPC_U3-0djt_i^{xVw9Bcb6HQ!QBTRe30e)YTs_v z{{7zD+N!O(x9+`j?#wx-PoM5z|GI0!e=10#qY|LP!NH+RO97PO;NWxL&chD~Z=VDj zRDOE9z`H0*iosQbh>qakzQIWYzN>m>o~-%ks=B?1T-<%+$b?V(&dfpo4L6AA-@BPV z+&HU$*awxTfwESM#a22?%MukEjPhe$TK4OY^kpb#^hY&Qs)lGCUsB&`vX`Rw-a3wvMx-O^n@aNOt0f%4_Jgjm#Set-*P;N-fAt{pmu7{kG} zVdqTRJbL7)Bfw1oiSFfVLjn=u6fjYV30|ZBd&kSm$-VvA5w3^pBSpgtB0MsjF2$Ec z0w=+b*jC|8ApRDPODXkiv9g7_UD}&C0c5?)q*?@0C-&2|iM1VphhGHn6hGwD^KV_?2g{7t0V?&Y7$=^t>Twk1 z!=eO~SHVc`XzD7}69Q@XFAPeGjLSMbw{CpRihkQ0Y4dh3)aN~0<906yFT6zez7{kR zQ(&`Ip*aJVoOWt?SpXnrCUEm}YssGmdU5?^W!XXv%BrX&&yoS8$j7#+YHVc13<67= zUdy?$r^*v7bj%8V{JTE-*K^^I?Z`agVNwZMnZ?;86m~at%r*w)e{}(H<_^ z#m2#8`}OIJJG-b`D^IWuDwclQD@dB_2uC`nsH?%xnQt*pjuF->Rg9`wn3&jt9^xu(m~oV>?(-2Naq#f|@XqRM60@R7wKooInBTARC=f}~ z1V!vM7MM!v+O|&+ZITBBg_%7|b%p6^uP@VTomOJ;xXww&bx6tXE|Xv%vwU*pvNBrO zsmZ0z7(N*!fd73^Q{OIMJi`7hIlA{l^1Y;7)`mBV-X_7-ScDWCCgu@Z-4`ILni*Af zM3xYlXX2{G;Ygq)^)RE3&o_;=1@^`52|WkqZ2rr%?_L8 zpRKVqy23sNX$oGxz<4Xn9fXjWk1?q*^Exp}0KiPEeO;H8&vj1fq0|Z6qGmO1zO^{T zR9BM~6EmZkrKohTrd*LaX9^2*l;+PvgOGho7yJQ*phR946qCacd!BojGHXck-EChrmA##(Lvru47Su)N1Q#LSqQ%ND00W zvGJK-mUn8EtE*sQCYhb<)mtu8e24$B2OldJ9j(Z)prpRaOKR{o0bhUT$Zk&>p554w zOuU34MqkO3!_*ieD$2~x+kJaM6llxYHM}LvCGJXY8xqMm(Tj6({7{o)8`j`# zcALlLqOfl-T*tL+A2~xK=xb{1P9~f3iE^Jw2Yc04<8*xPGv??In)`~|HFgS}zb%xv zSqbGsGfU3m?rr1(LVQkJ$;1^DrcEi4@hKAl44qc&GZje>Wn)is{m5Y7S_h|ZKLCK^ z9R_n|SlxsF6g^bo?I2WYI>D;@f{@TyPP0MNwb$7D6FH50d#&O%M#G8dj+c9u^0I_u1*s_i4 z^>yq0)mA%DoZp&{2ig|Fu?^OMe_ri7ic)oU$YRdy?7f_qu5wda|pK{-({%l@He zJc&`_(Z@$vM%}XIVuLoh0Izd-CK5`IpF`gx`**ku^2lL=) zrX;uVgVOD>x(|7HPS$b~J51)E3|D6KeMEF50fkbU_Od)y7YR9`v z#7ehK-@Y$mMsKhmNf7Y2^HtKgu~~TG$st<$`6}W+rPVxgD>KsftdW6x30b%JZClgr zJE1*4KWVXaJ9DqWF-A~QDe~Go7Xp4gpviI@3(+9a_wHyj>Z_|UmRz}YycY?uUd<;o z0M)At+}8pRRvyrvuP2?F^cqezWT*FOA=d>4z$vc8c3{)czkrvY2Au(2b}7QIjc%t% z=i-e@twmj%orjpfJBjB@yyxY`X0zdS{g!K>hX>I}hqgnlCZkBl^FOzTgyt_?upqN8 zJlJ-Hg9D7bpmg21Ru#v$5xN%8(criso5S;r$(Rglr|(@hlL{YV4C zrqYM+T#F9tzpCv%{vs~hSL@ERm%q3hv4-OR{Vr> zrp1|4pE128x|>G2m&i@0S+v0u`$ZORtBa;+V7Yi50ViKyLz_qQb>1@BZIug|5j`RV}&_dGg&!QUKtccKF1Hb!)-I*~~H`f-|X zysVz4njvSR_-%>Pn8D z|4f{Hvhvw%!ho6wITDdX=VXodnfVGP?zs1Eri`-9&f4S6cG4 z#BpVnLr3dPVV!uzfD(P)f_(G7tYyB@>D|BDdG`=PSYYeccDah$Ty6xLl zep*Z-GnC4b>T6F)#Uj0%3n^bHu~26J2I;MShRr$=A%G&77}Tva$&D$6h3m|RgDo8o zPww>!)BkP%$zJT6u$^arb(bJ*DB`_LTD+UItXvzKEtUvIdVr1W!3&Rh3=L9R3Dm-& zm;gUg)4sI(*~MyLv$@BZNC?9k=bM}yp@_!RRpZ9tz?CjGGxPqNv5>~U#V5&kF%q+1 z{Xb$-ScNrOH-?mof9G#p;I^7o6_O#~3dr9Dw1qTirFoehC!i*W(S9?x&dB*5ZIL*m zm19x$$<8I5T&aIJ(ssKp3)^J;i6KAdl0eb=tW*Fc}lGHH2~5 z+gHg=$z5IFUtPUREH0~Js;*F@*pJshzRZB-l4bETtj``v7O)*SQZ`#4hUokBgt!}z zEnDdUz62m17o-J&w8(vmnWXa?u~q zzMhQzgdmQVY`N3};=tCdx#Sglj(gP(Bcyl;AQ=)YTfY^E%0_^R!*^(uNu;v69iI_| z6y@Wg7&KaH*72+Cdl;w9>=#n$iL>1gKGL49rEa?7#|wH$Ko*xTrx9MXmfP2@EQ2L( zW^!=YS2GMs3)+dW?QYdZT{JqZ1!Bvlmiz&?(S?Bog)j`WFiXc>RY>rc+kxBriDG|d zz24#*1MPw-0eb#a?@(G-_d*)X+K4s6*#-b*8ZSp%rz3hA0W%G`F|^4+&Tjoj$!J){ z=Zlfu)Io;3{W^f^8&2K7M1lViw9kVcLpHtXgiaA!6(BGVc5*2Ze>X~++FG<^Xrd)r z8c^?PC0O7=ygKJ4$#3TDtUvw-ih}YcYX%0R{!4RG;=$2Z8LOMT*&}rL%>3m)9oI5* zTRh71fb#1Wnjt$B429RS(`eMD6effxryD+q!I=EPT#gN1i9iKKO)tfEN7|L zr7`F)Cq4L+$v#$A5waIQGxR0t7HCvUjp!$o(Aaxg^ua7yCf*>mJe`Y!{^uBOmf*#nG-Mt*$I5{b?Whs=9X2k9T6Tku z^a6Dpub{azEJf#cN+%!RUa%`y5)Dg^HmBYG32W;g&Q`j1EXBls04;)?)SHi(4^m}P zO8I1Cz7}Us`BZFM>sU>?adm#O+?bfsL*l_QVPEkJ##F4sQF(*~>UrJmTUHY|gVRt} zsuUfKc2x?Vq_M~?;>Wb|D`QhSfF1WOy7?+N#niSk6E@E%c)T{!a|=AmTnlljdu-5> z5Gm;_H^+l5UwKH(8p!^`w@rc(bNkgi{idgWIULL*H7^TQrci#VAUylkj=~+ZkIjZ8 z0jJRyC-Z66GrPnQK3jiJ&2mOrfXepIbuNk>pOF|>YRm4IG_Y{IqpwW+U^Lk2&oea6 zc6}LoU7EPO=MK~1vI%N{a)<4NWI)Fk(J%izhl`CNSs#!4677QNrW1-p^(a-&MRG$xmX~d#57|>{8g~zCMiO%D@6}?Uu}{%oXnnyjf|ssF&- zgLJrq7B*UH{@Vf%z#1nGVeD-lmMfZkD+UcpQ1s+%0>8-CO%Vz@T3RIwh z#656HoP|iaZlD}Q*%dI* z5tA(ji?sXS2)>g5hK0YMPILGDini}l zUoq3_y_&akB&;E9#Nu)2dV)nA7)wN#!p{zf8sIS8PH@WjRC3n2CtO8B6B|=&7pb_b z>!TB$;-VfTxyVm7mxFfK{uZcQ!`cByqo%KMR^{A`29 zyLJjvNiNe)n~OTcBRW#1D2y9u94{6o1?G4V%|w8U&T*|?eU(Ld{-zl$)>iA$G<8*_ zPt#+LgYb@27yHc1#;NY&lS(X?z}uMflkH{J1;O3X3!4970x&*xylWY+#UKBTOSFK^ zCt(Lt3>#{-z{^EKc1$rGcb>f+s#%>W4{CY@L{#;IXei;17Pt`HRD;%XmxuJ9@%%6E z_WzZ+{{Nl+FIA=gU%dYx67PTaJ>%QHhigD$`k!TZ-VHz?Sa5LO@3sHu8>;_HyOIOn zhK&FxIw^|wzt2qmxA*S`c#bu7nvBt}=O0O8!S?}hhr}ORR*bkaWXtWTO#?i>-G%l+ z=-X#wi4HjgcHNSLk$UK$)9tTSi7zYcPkFI-THwFer*1v?lx4y0= zF?eKGo$t*j6RrJo-YY5LUsvq$`_NMT#&qdf2h7hy*t;jo$`{sev9z&9SJ&27Guw8S z|6Up902INi*y4R_Xc|gBe-(!PJ+36}?ABNx{=QUi+iT`e`8;idhdla!!@Yd|>?jm$ zc5_4nnwwh)7Ub=-$!*9;E?<2;yCtewI%g(%0Q<92_#-^7y zl%~`yd)F}49cYcf7|X0B zxTYiZ9ivS`ktT0%9BE21e?J3ItjbOXq$+tA#qZYL%HlNCBhxQ?>5+6k&HpTzSkaRLPz-3Gaa$O)mXkS* zCPiJ3^EXT4v1AJaq&T@pB86A8tLp0uOxL_mp2)s!o{az_%Im$5ioo+bKZ2@$tbCN% z@hI`HEME}*%F5~3(our;S$549ewPwgAa2^y_Fmo$bau+01>*5uPb71nwCB`#({mz% zxzwP#t^T56b{Q6pnK1Kn`n#T~o+!ivtc8@vvC#Xm&{))ut~BI7j`rBeO@?R~IMP`f z>SjcLXjk`XdeQpt<(bch&Ec3%=`mCEUTj7j0{=%XfT&mh7!?JzJn6JT+$m#TL;Sm6 z^0I%??_6wnHUY*ft@Wa!0EPUJ9Do>kP<21fbXe1!sE<#G6m8+yZxucb*?F8KoT8#6 zfN?>bluM(k?DmurdH_M5;FLz9-qNKx7DDy-XGf0#o80SPaWCCpaI?lhgro$v+!-7S zSyiPoQ3Pb4h^BXr7T5}=q$_9jfKsERovfv9le!QqfmXS@TC|S;mX-Ktt(-r>>}jI>ssdE0f$^5iVt z$>;Px29waDt<+v zG9z*)KP@XHl;F2_65=kMf7Rtl0fP%*-NHUaH;aEUlE2+JQ|!zaQzhzktlE=i`kmI4 zb_z{_AR&=_NZOaFUb6d&Ih_6{w5=mk|KyF!GQ#L<=m}wTvdjk4OL_vX_}Y$Z-wuCm zc?W%eXpaa(2ze0tx+rvHu8Oy7%GIfu)qxNc;>!C24IxhSkkj9W#D{MY{Oc;03^U>R>XX^Yl2zDOjz2O`4h21&dkM)H%ZKW6#y8WWypO|A zMLq>A6;4r+K`>~I8hM@`>B*#otUA|jS>M)1MRHQR?-5DYKkWVYmx$L|bqLvpG)g-? zk1cQ2E(^x@WSm`C8(iq5OQb1ph&gsz>5QsizNR}l zJ_mu|0sw`c4W0kVqA8jsBukC7+qdl;p_s}ht84# zVH$qd6W7OI2+mMq4k~_HD!P^RnBUcXz*{{N_B2=itZ|X-Mq6P|!g^@(WaYyqTAbAG z=H?XQe6LVZ*ZStJ={Qq=@PG$4>h5G`yW^MmO2RxO4*>~`Yb9NVv*Xj!2%A9)g?NVpzf5+en$Lo9b-}4})X8QWuV*$Su^xo6vg`!W z4liE(+Kz_pI%GSmNy=3U->}9*qK2MPh2Y@9T;}y@W=j=3NIlas zQ(mN#tpAMBU`^zPIznLWDkhRiYutcMDc#18N9@$&NW^G5*4 zB_$<=b7BJ#50O5!FxQ)BPFr#kM4Q*QCcqCLK#>rBiGCApgTE$tw|e@o!PLWBU$#@3 z0`AQT$Z2%r;lO!g|Ch=L!uelAUi{xN!6x_qbgixXZCbrW06iUDtNZg%qYb#)pj$~p z1B|!_-$NQ#Uk}awj);sw#A~?;=iqQ&YeI^OPE2E*TFqR7wo$I8t?j*=VUk9pkj)3B z7(Z^;=YZ?6GyXa}<+RgBqmasPH*`wg z=p>8pWlb#!35h#R6SiYxW8+HG`F%Gr3NIp3V7p|u|MSbE*hyqSw40ODkzrj7LUpMW zKCBO`=*oFg^#h!vWC+I1#`8lA*$>~9jNG+~(h>zf5ialdGw5(<+TAbOoLt^Ru_F5o zJl|K~n>`$6I*`z8tgXH4xZwf?)U>qt4$>YB{Jlzn8%>ywK6liE&(MGgbC*f*dpO6E zkMH{{7}U?!GF%}w+IPKKLK)Z`7TMF5CxcgTaA%34-*_)_3PVXrW}gOe;Wf0iAMX>7 zaBy(^lt|!uh@Ct!HK;qL+j zgf(bUVWq5af%ZRt<}TLV)DMx`m6V#99fxBkG-})BEc|CMlvqv+cQkr>RPszMOghaJ zYjA;xoCK%IRV!7m`&x|2b?k17IneHRc8O%!}Z|vC(Fyq zHfQCND9WEhyx+>|61W!bwX1GJD^I$Z-+Qm*yb{Iv8g3vBwz_zUoGo=idyz>GArWmd zM1qjPl(!uEwHvXc$0HeoP7N5^Jw7s^vi@R;JIuUXZ*x%E6WA{qlgm~++Rp<4UN#Si2o}o4T|}!3TJ1NOv~`@ z{ysEPt-OmF(R&_t31Qan;Iz5bwQ0kg9*j&;o}CFrzwvNw*qDe@&}nBIl2lFZ^nd*G zkH%bFgpF;tOby>`Vl%bT3EQVWNi{p^dTW6N8s{*TZJZQQLod}S4nei2kHIO+U`K~xu1a9LJ*$4V59yJTpYTJ3NFY(b$Gz}T3A>(fHDOp#J6|J zA(A~0IgQ3OMi4{zN>m0GUrx`ggI&o?>&q8iYe=q~;`Vv#0*ja`Mk4goP0WnNY*9afsGa2Hm~U z{LHJpM%xcBx@ugw;(N-@9sCQ_c-ebdsBN<$iV;lXDU`IV%n`7$IqNQMy>+meXspjZ zOFhraIr98`dfl6jG;9UkpylZNZ8#SZaQF3peZC`>dx=d*m=!p$gbc(FV1-DPC^dVS zk3{L*&Hyx&PyLMce}gwPFHa`SM(N|lnIqAyGyecrIA4S3FVAcAFHS-jiP5v0ShdUL z<%P8*uDbUGM|eYYuLx&mbrJaIt&Uxl7U`St*Mq5EskFG8;=vKbvx0Y zCAM1*sIj!f4v48wp$Xq@d?%F3%>HlOeIg*hz1XDlz5`y$8~ zUmaN=M6$C<7+zjJiEjlfFv?XJg#)mxe89|96o|;!Nm4YUTO%N{iR?5war0VEX7*C$a ztHjfKdKY?VUcbY)Vo!*5wKxxtp~~-_ec?7(4-fx^U8tr^;(6;TZnz4{Fvy@tTz=qHm(YEtmu7|^B zJrf_3+Coic<_qo@?9n0drJ=0w{`}v%;Spbgqphf7(;1tAyj;2pg(^6-H*dvWf~gfI zzYFxkQIeFkyp6SavZbtJx`mCF)q_D?oc7SMP38a*{?!CideTr+lQOENXWxeZj@NV zZY4bt;fGIewb;-)ork5iU!Ahw5x-i=BcIk_T_Qok#iR@>t;2e&@vLC8kx`CURK>=m zQ;vtWb8J*D_Wc2aNmvSc*$BBoS7*0_{c94`NmpHu{ANp68n8C3!|$Jj?|Y^x432Id z3x;FBU`Vli*7o-H)o8Ke>s^7OcWeA+^DM^jn9B~0q=OCY1)thkWdT$czuLH^csg`1 z*YZP$JAgdw!zVn{;1=Sfe*c$;lvZfYO52`+qI@iM{^R3gcW2%9)GMs524YQTIF!B^ z{wpuRtnkGf^1YC7l`QY){ed(lLO9?_O7k#0q2#fRH}M^ zYOx?mH-EJN3ri2_+?y9;W@Z+A!-vns2VK?3aJef3-FIidk)ZbgPQI(!H@^v&DM903*Up3F-UXIIyQR9N8dp5uEPEG&4UTX z__%{Y+M}wKl@;my8zg|eM<;N7It0IAr|6mnPu=*E63M<8JA_}kEY^r{Eq>%7OGy`6 zP*)pbN5FLtV!=XJ5StU6W9-uXbcj*`f==h+5F!g6V=CV3db?QOzh>0`d`T?Nu^$;y zP*Ae|$e{0Iz!jeWoqYo?+G=X^(7m^TQm4taL0_AK|GaB{SaC;;35$qSi+pWs$Q7MV z_)mLrU>wD*f2~aqoR$x^>lep`lJL{z=fQ5_HD$f%14M+LwZ3_^I^U(4#REL|C$lbh z1WE2*DdXef-~=%}dU$K|3VmVW{9*UQTcG z?p)91`Fi7xh{?x~N=zIaBlat@J^`>g*$-^28MlS!y2F^8pbDsD@$XW1uYWRE^Aba*a>z7{T*q61I#k!27 zu?<-g3=GFjk5?qixz!I`&CVz2mE=Hi{!M;BWcNN_Y zDRvkFEcg{xNMiq1?_1e%b!#pO%=(k=GfceTi?p9Y z)otrs?d=|y{1XbtlW_AUCdrP(=qeStq*2Mt#;b5n2Ql*Fd!X4`d@CK{}n5Ihp;^jAPY@R-_HliU&SiumF7+`Xkg{alQQPh3rpp`vOQ zJq5O$HDg>$>t7)=$qagYknl>X23iT~ihqiY7rOm-cW9KI)q)(t{6!D5o!8@LJ%xEr z?s^A(HgDtO1}@?F@yd(OXr|}Z`tOo+JVAD;sWima5*zA#(rVQ+Ad|GxYD=-115>&r zr__WTKAFzvZJ`%o9(@{j*I2ME=Txhl%!)W)Sz(o>4?!(5l6-z?@E7*9cYVS&OSZLN z=F(%5Ad2dT^Y3Ld94yi(UNL#Lz9{a0$NO%fe0FQyCBRpyRbO0VbNJrtfF&A(Xsnq> zbK10t2$tsw`2}so8-utG-BMRrM2ip3zdhj(^-k(+8=mJnCbc|O{=~S%U!>sDp*@QC zw~KiRg%mHl%NA=>;Hai58u0M({S0gm3hLZxv9RIRiNBK3S5WfRaaF0{Y2-;$cNk-s ze`+pW9aOs5{N}WAX2DKyxH)|7MMPI8Ww3E(`sm$Op1bN0?#X#V=KRkEV{UeM2(!_D z=iJSr9KEsFU5CDrdOTQrHg-P5AK}WQDaWZ<1G*ND2JcaILY*^7_^r`JBWu+5;cPfw z{`IA(O=4r6)0ZS1#)on3+O{FPIw`H`*(yW33GX$jigD;U>rDqHo8Zlh?={Nyhwxr_Mi4IFZg>L# z1Pk|7b-uZ1*t=9m73rgvxi!sG)y<_%I;!Tt+U=~F$C0Z4GV4d2 zQ*kQ=TxeR!o;S5UNT_**yTX?MGpo#_6Wq|E5Yh2uMR_lGMttOdl+_(RC>L_IEnJ@q zxbUf}-m4|UxsnJ+3i{f`k)vT!r)vU4;u^XNc{i8FNNP~E1GP+#I$Af`sU8dutSCh# zGCJsM+HNlQ8)kO zU8q-;Q)qct^-}HDKuL+|#K$x#^4<9~M(@bM!Cwa>J7wtmdQ@%a_RHR=$MVUO5s)(v zXHU`9X`k*2u8qdC2P5(~FSz!&!q>=_&Umym2zF@Ns1=)=lx8LK%;j2}z?HS@%##^c zx6(>jQZ6%l^^qtt5kQqh7T!?XR#qxHs!rER*qd?)iPsjO7$<(%1ziY=3tGTyDb`k@ z1D&(})AV=?JMqpp@0{*nafX0v+Zk^44a6Uz!k3Y0y5h@ zu;FJ=t*}CH<4`xQrTMPLBwtB5sLCIzEMGN^<H$vo+cy#3xE?CX-RUEGB;_Kg{DAf1#7hpvLI#Ro9wq8JbXDr!O*5zp;cR|5&p}oRh;{ z_qmAHzT;`c4<1q1yULutT}GtHjq_JeoG;?x<2eAp<6m+UK=z9QC(}Ot8%>gGisgN( ze#Jv^uWxx>Sts50?8dnV!9Js>hL+D;t_grZ(u(3au#;86*5$1Pd;IHP^U+vt<2NBXG+9>g-v7YnKVpH`(9t;GKAF4ZeDjE>(I2b^b}lpVS2kcN`5An{)+Y70 z0YxD|rBVS5UicgXEwBUA)_&T!-=AX%jnE(wsx< zjwV)WvxoPB1bX*%E>_8E>EoV!Qy!O-{P;_AX~#|=WIg@pgl%U1#PT-`;b`c5U;z^| zEy|U-{^s2>>Xs?WC_yh26X(V`pdsqM$+WPSAU`->EIi3HvHs%BI0r zU)y6d#J;k^ur_v95w+@OQHhYTmB3^e9{iAgZhq(6b9D0_f_*6Px8SFTxl;7nibTQQ z!mZDG@YPbn9`VH4A#H>IQm9^5*@LZC~LoOI0oFui@KyhGLTjJper7^&O;WPqH5~t-eQti!@8>Tw}kt-nk)9o@)@P$Sx|P) zRoJtuPoc-n0ca6n2Wt)L-BHFfhfN@#-&wX@ZBk)&;&J(cx;h|S-sF#Eg&KDp&yNYG zq`t_gk;UOIr3$hb7+?64bTgt|qjdpsky1L+BEI4hh~aCrP?1EpRJr%$(U5c+BXIG)4O=B z0046MS7DD5tmx`G5UcXOG4U$lF=U_0yb|7HZC$Lb6-fg;6}vu}wEq+oA|ki&$KK^{ z)nYWYhy_GB5j_u0M7cF0tW|8b7Se(UH!r)^`;P=+g!%c>zZM}Li!_` z=b2dcwCzLHy6JOZ!)N>qVwJu3KHwVO>EG=KHThVWu_xC?9v94bAMS+_b$#ErcuK># z^xLrRTpGF#DX};+z##3D6_E*v#}ha2%kT6PP0vlN8OgCl^0K40 z4Qhwq4tEC=NQAf_`7Ztlma}Vj0DLvbVjDI6!D)t~n!l>!x%z z{u<$(KAi2UP(4WYCtgX(ymi9^m!Tid8IyEXbN`}$9O&wu_*U0tS+RU}t21|SiWEVQ zmCW#;&gn8|Z*XzzUNC%k4T^sk-Bj^G}=0BUOEWU3C|=5s;HN z#dIwxB!TU^-LUS=frzQa)hIz}6M$zslTL;|VsLP1U;p#J0Bv4kBPxpbx!zfq zuIpTAm=wDroXW?WJv8~-gj^ccR!q3cy4AKsFZ%~32i~pghKBrppI53L;640no5Ull z7DUSlIC8+ZNi_*YL#tN{o%Fx#-MiGQeQZbsT3(ar1;A_9)KE7j8#|)8qiAXsh`QCk zlq?p*^YwIT61ncoZ{+M5hkf|vRn#djyE8fZubHZNbcIIm94tBcTBibGqeUWGh}Ena zQRvdHuZW#m(0~3IMd+bsY3r37TE7SSf_PkcZp^JjLif5r-t~i~Wa3p$srBFdExVPB zzbj=~d00ip5VY23-Te>WHT>c9?#sK_3*IE~trX~rer+XZ?{i%HA8Fk?3y59t#({z2 z{p0UeGaWk1)U0zv3oW6Y#WEHbh#ZhiIkZ4STUTm6<7n34+L+AY%J6%4vWJmg1=qRi z*Yc%>1GxgB(|5Xe3s0%t!@UO8Zoc~_O@FJ|=IqlFB{`=jL3%Q4Ivl|YjYve8da!8w z zWQTt?NnEejE>0f1SzrQ@$IdhOTEip5lC9@=X*eOo<&Wvf%>#lb)(4#%|EF&X*UOy1 z6<8~Ji&Ni%I-ZKtkHHYp05o!`MbiGtA&(W`$Ds<2q2n1}LRXv1n&GbHtYn-C|ZHr_;F-JkzY-I#TMPpK8f7-2E-#;fnd@l!O&4sb&VijYyRRs5(Up!vv13 z3DkGKo0MFe1M{lI(kM2(%eKlvPikprgT0{04sn$7#{)FZ7{NN>v@JlE3=sE1f2edRB{)^1&cDP#* z>oR%X;G|+$Wni1y?CW@NnNUi5S^pTeQ0(Z^8J|Z}#>(jt1!&#rs8G zQW_;m*qAN1$4gf-Xl~--=m>^HBNNSY%qU1QpJ6%9a)O6}%2hpf9a|{e`8~ zct)1Bg##kiQvphmw8qS>=(#W1rk$Z%=5){9iia7m1;^$)L$I5Y(%`0;*7fqGxJe&n z`mt*6RYM@4kYjd#`w_LkY#+{i_Cqbmwv00sNiaKf!Q$dRK%FC!GK3 z1fmS|sP_@xtW*^Ksi8V5$ln&^aLqdDr4nK<^waPk$3i|Avhev^{RTRTQ3;}go)VQ? z^Q^$Su7$O~>EBIlI-bBPEpy3JF* zR#s&BlxuU4|LQOQ@Nf@1S<7ERfN~+=(xrcQr)xd+4UTDe}i6Cz)5B24b-{ z`g;Eg`fJ{`;7XQ{XX8ILxaH+g5?eE;MVeyzZz@}PEx_Se^48_+2yfW^Kspo8buwDx zkowWkg;(vm!l$-Ht1h?3{dQbD1cl@2S95W~gJ74!gzE6$yB|9-|juY3s!3wrRm0d4d&UFy3aHh*>xKlI>2ayH3G{ z_UHO~BP`6lrfx0G(2RRF`j!-b_gf*KgYtw-`zPk}@bm}4bZRqCz$?thZs*?y)`D(d zdiZju#MVej%6kDJrux^Lk9t)P)ozn_y_nOYWj;$lnS@bL6^$5tz;VG3{kPmwMvF@S zHs$l5TE*B*zR}6E=E<+o#aCm;@Olr~|3?6J1d02O&eqJQXNC&idPw9c>QvX38^e3+ zA<@EyJMjCaW(WQxKljlC#orXF6Z1;1)#!;RySO^xtSaHGYG3}fnug(~*J||tK6foe zQ8vuOd9uH!ur5o4#vu;*$P@MxNhdD++5)82?S zD>otUWnbr*fg+*3c)N|7?^;}VWH}KD1%m793Qv2DYCfMg;=cYP{oDi&yz(kt^GCcn zF@hPB`~(hsB2tyrik%&wn>PH<4|`i*>DZ;q#e)WTio4h&+u53VykMzAu&0B-fy(ME z?R_nS%!&8peZs#7IY!?v?94Zd^mNT3-mZ@faT7Z5OEtQhw!W5uN8}}D=8X1wFKT$q zgM;jTAwy5sOb!kd@CXeXLnb5Lyw>j5GKk$NbSf=eOOu}?NBf_YJ-eTY0{RI?O9TQQ zl^ZN|?K~S*>d1f0!*Q9f^CWk>AAGl8^WBLf->OAKKji!p^1yHr)2Wd=I@VNLUYny( z=UphjbhWy=hIN;S_O6}8I64qfZM}id=e2sYZ+q%>Y<|J7VGoV!>6+_}bhXC(t;(3I zHl439-Keuzhz2_I0^J-&dpbn<3I@CI?G4`S^_mT)-zw_=TU4H*y!lf}^>r;vJl;ST z$6znliGJ>n5BvY^-BE9vP!z!NQ(ADgYqcnITV1LR-5Sl3nQZeTEZHYs_OiWfnOW+Z zG$c)nQ4`v93W#grqNx+`g)~)oqw72au95Ggw6hS z&EiXQ*bp_~0H9dNm&!sg=PH$jVj<`KIM_TJHv3WJ%&+E9A!iM4LO09{vHm81ywJ0p z30Eilq1$z9C)hjd`+7-U&iQ`Ho;Ew|cyCjC6+u%U?W zusImmOt5tsaJJi~Dyy2TXz|!?ieo8iBf>BdhGuBm{>z3r7#OBGvc|s~O8c(Mv9V-g zf4N^BV>33B)Euz&8gLV;*6IA|cDsGU92n-n-UXjw=mRsaA1 From c1c98d37fcb71b6c0f99e7bc9e2d58d5bde3bea5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 07:41:44 +0000 Subject: [PATCH 75/93] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/synapse.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 74625ca3b98..7ee83c3fe65 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -5,14 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { - AbstractStartedContainer, - GenericContainer, - ImageName, - RestartOptions, - StartedTestContainer, - Wait, -} from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, RestartOptions, StartedTestContainer, Wait } from "testcontainers"; import { APIRequestContext, TestInfo } from "@playwright/test"; import crypto from "node:crypto"; import * as YAML from "yaml"; From 5e9066ec8cbcb5f712b091f8dd5e9eb543779a17 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 08:22:20 +0000 Subject: [PATCH 76/93] Deflake more tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/login/login-consent.spec.ts | 6 +++--- playwright/e2e/spotlight/spotlight.spec.ts | 5 +++++ .../homeserver/synapse/legacyOAuthHomeserver.ts | 12 ++++++++---- playwright/plugins/oauth_server/index.ts | 13 +++++++++++-- playwright/services.ts | 8 +++++++- playwright/testcontainers/mas.ts | 8 ++++++++ 6 files changed, 42 insertions(+), 10 deletions(-) diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index d7d5861a02e..31d0ccc652c 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -92,6 +92,9 @@ test.use({ }, }, credentials: async ({ context, homeserver }, use) => { + // Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts + await homeserver.restart(); + const displayName = "Dave"; const credentials = await homeserver.registerUser(username, password, displayName); console.log(`Registered test user @user:localhost with displayname ${displayName}`); @@ -100,9 +103,6 @@ test.use({ ...credentials, displayName, }); - - // Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts - await homeserver.restart(); }, }); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index da35ca57b35..22a3a41a811 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -81,6 +81,11 @@ const test = base.extend<{ }); await use({ name, roomId }); }, + credentials: async ({ credentials, homeserver }, use) => { + // Restart the homeserver to wipe its in-memory db so we can purge the user_directory of users + await homeserver.restart(); + await use(credentials); + }, }); test.describe("Spotlight", () => { diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts index 246829e422d..90737f94a4a 100644 --- a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details. import { Fixtures } from "@playwright/test"; import { TestContainers } from "testcontainers"; -import { Services } from "../../../services.ts"; +import { Services, TestFixtures } from "../../../services.ts"; import { OAuthServer } from "../../oauth_server"; -export const legacyOAuthHomeserver: Fixtures<{}, Services> = { - _homeserver: [ +export const legacyOAuthHomeserver: Fixtures = { + _oAuthServer: [ async ({ _homeserver: container }, use) => { const server = new OAuthServer(); const port = server.start(); @@ -43,9 +43,13 @@ export const legacyOAuthHomeserver: Fixtures<{}, Services> = { }, ], }); - await use(container); + await use(server); server.stop(); }, { scope: "worker" }, ], + oAuthServer: async ({ _oAuthServer }, use, testInfo) => { + _oAuthServer.onTestStarted(testInfo); + await use(_oAuthServer); + }, }; diff --git a/playwright/plugins/oauth_server/index.ts b/playwright/plugins/oauth_server/index.ts index 3f80dc11ca9..df5ee0f461b 100644 --- a/playwright/plugins/oauth_server/index.ts +++ b/playwright/plugins/oauth_server/index.ts @@ -9,12 +9,21 @@ Please see LICENSE files in the repository root for full details. import http from "http"; import express from "express"; import { AddressInfo } from "net"; +import { TestInfo } from "@playwright/test"; + +import { randB64Bytes } from "../utils/rand.ts"; export class OAuthServer { private server?: http.Server; + private sub?: string; + + public onTestStarted(testInfo: TestInfo): void { + this.sub = testInfo.testId; + } public start(): number { if (this.server) this.stop(); + const token = randB64Bytes(16); const app = express(); @@ -28,7 +37,7 @@ export class OAuthServer { const code = req.body.code; if (code === "valid_auth_code") { res.send({ - access_token: "oauth_access_token", + access_token: token, token_type: "Bearer", expires_in: "3600", }); @@ -43,7 +52,7 @@ export class OAuthServer { // return an OAuth2 user info object res.send({ - sub: "alice", + sub: this.sub, name: "Alice", }); }); diff --git a/playwright/services.ts b/playwright/services.ts index f275f63774a..26ab25c5c6b 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -15,9 +15,12 @@ import { ContainerLogger } from "./testcontainers/utils.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts"; import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailhog.ts"; +import { OAuthServer } from "./plugins/oauth_server"; -interface TestFixtures { +export interface TestFixtures { mailhogClient: mailhog.API; + // Set in legacyOAuthHomeserver only + oAuthServer?: OAuthServer; } export interface Services { @@ -31,6 +34,9 @@ export interface Services { _homeserver: HomeserverContainer; homeserver: StartedHomeserverContainer; mas?: StartedMatrixAuthenticationServiceContainer; + + // Set in legacyOAuthHomeserver only + _oAuthServer?: OAuthServer; } export const test = base.extend({ diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index 697ef374a18..69397894574 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -162,6 +162,14 @@ const DEFAULT_CONFIG = { access_token_ttl: 300, compat_token_ttl: 300, }, + rate_limiting: { + login: { + burst: 1000, + }, + registration: { + burst: 1000, + }, + }, }; export class MatrixAuthenticationServiceContainer extends GenericContainer { From c8e46251f526b33cbb885321dfda9fcc0ee7e056 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 08:57:08 +0000 Subject: [PATCH 77/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/login/login-consent.spec.ts | 6 ++++-- playwright/e2e/spotlight/spotlight.spec.ts | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index 31d0ccc652c..3a35e72f136 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -91,10 +91,12 @@ test.use({ }, }, }, - credentials: async ({ context, homeserver }, use) => { + context: async ({ context, homeserver }, use) => { // Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts await homeserver.restart(); - + await use(context); + }, + credentials: async ({ context, homeserver }, use) => { const displayName = "Dave"; const credentials = await homeserver.registerUser(username, password, displayName); console.log(`Registered test user @user:localhost with displayname ${displayName}`); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index 22a3a41a811..5ee80d6ea71 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -81,10 +81,10 @@ const test = base.extend<{ }); await use({ name, roomId }); }, - credentials: async ({ credentials, homeserver }, use) => { - // Restart the homeserver to wipe its in-memory db so we can purge the user_directory of users + context: async ({ context, homeserver }, use) => { + // Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts await homeserver.restart(); - await use(credentials); + await use(context); }, }); From 84dfc5d22d84efcee2d118f2ae6448bb77b0b319 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 10:43:56 +0000 Subject: [PATCH 78/93] Fix flaky tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../e2e/right-panel/right-panel.spec.ts | 1 + .../general-room-settings-tab.spec.ts | 1 + playwright/pages/client.ts | 19 +++++++++---------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index cb2a11ac002..0bdd0a283a9 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -47,6 +47,7 @@ test.describe("RightPanel", () => { // Set a local room address const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" }); await localAddresses.getByRole("textbox").fill(ROOM_ADDRESS_LONG); + await expect(page.getByText("This address is available to use")).toBeVisible(); await localAddresses.getByRole("button", { name: "Add" }).click(); await expect(localAddresses.getByText(`#${ROOM_ADDRESS_LONG}:localhost`)).toHaveClass( "mx_EditableItem_item", diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts index eec32f7af5e..5e29f802c2f 100644 --- a/playwright/e2e/settings/general-room-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts @@ -41,6 +41,7 @@ test.describe("General room settings tab", () => { // 1. Set the room-address to be a really long string const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4); await settings.locator("#roomAliases input[label='Room address']").fill(longString); + await expect(page.getByText("This address is available to use")).toBeVisible(); await settings.locator("#roomAliases").getByText("Add", { exact: true }).click(); // 2. wait for the new setting to apply ... diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts index c2586f1b5ef..362915ce71f 100644 --- a/playwright/pages/client.ts +++ b/playwright/pages/client.ts @@ -179,18 +179,17 @@ export class Client { public async createRoom(options: ICreateRoomOpts): Promise { const client = await this.prepareClient(); return await client.evaluate(async (cli, options) => { - const roomPromise = new Promise((resolve) => { - const onRoom = (room: Room) => { - if (room.roomId === roomId) { - cli.off(window.matrixcs.ClientEvent.Room, onRoom); - resolve(); - } - }; - cli.on(window.matrixcs.ClientEvent.Room, onRoom); - }); const { room_id: roomId } = await cli.createRoom(options); if (!cli.getRoom(roomId)) { - await roomPromise; + await new Promise((resolve) => { + const onRoom = (room: Room) => { + if (room.roomId === roomId) { + cli.off(window.matrixcs.ClientEvent.Room, onRoom); + resolve(); + } + }; + cli.on(window.matrixcs.ClientEvent.Room, onRoom); + }); } return roomId; }, options); From d7a1c307da7c334e6540025b62b08394ad4552ec Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 11:10:59 +0000 Subject: [PATCH 79/93] Fix flaky tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups-mas.spec.ts | 3 +-- playwright/e2e/login/login-consent.spec.ts | 5 +++++ playwright/services.ts | 3 ++- playwright/stale-screenshot-reporter.ts | 6 +++++- playwright/testcontainers/mas.ts | 8 ++++++++ playwright/testcontainers/synapse.ts | 7 ++++++- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts index 614bde50646..03c83efb1af 100644 --- a/playwright/e2e/crypto/backups-mas.spec.ts +++ b/playwright/e2e/crypto/backups-mas.spec.ts @@ -62,8 +62,7 @@ test.describe("Key backup reset from elsewhere", () => { await page.getByRole("textbox", { name: "Name" }).fill("test room"); await page.getByRole("button", { name: "Create room" }).click(); - // @ts-ignore - this runs in the browser scope where mxMatrixClientPeg is a thing. Here, it is not. - const accessToken = await page.evaluate(() => mxMatrixClientPeg.get().getAccessToken()); + const accessToken = await page.evaluate(() => window.mxMatrixClientPeg.get().getAccessToken()); const csAPI = new TestClientServerAPI(request, homeserver, accessToken); diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index ab70e1d1869..a87b5019df6 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -88,6 +88,11 @@ test.use({ }, }, }, + context: async ({ context, homeserver }, use) => { + // Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts + await homeserver.restart(); + await use(context); + }, credentials: async ({ context, homeserver }, use) => { const displayName = "Dave"; const credentials = await homeserver.registerUser(username, password, displayName); diff --git a/playwright/services.ts b/playwright/services.ts index b480cbc4054..38c4e6c6664 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -131,10 +131,11 @@ export const test = base.extend<{}, Services>({ { scope: "worker" }, ], - context: async ({ logger, context, request, homeserver }, use, testInfo) => { + context: async ({ logger, context, request, homeserver, mailhogClient }, use, testInfo) => { homeserver.setRequest(request); await logger.testStarted(testInfo); await use(context); await logger.testFinished(testInfo); + await mailhogClient.deleteAll(); }, }); diff --git a/playwright/stale-screenshot-reporter.ts b/playwright/stale-screenshot-reporter.ts index dc934827c1d..36aba56a071 100644 --- a/playwright/stale-screenshot-reporter.ts +++ b/playwright/stale-screenshot-reporter.ts @@ -20,10 +20,13 @@ const snapshotRoot = path.join(__dirname, "snapshots"); class StaleScreenshotReporter implements Reporter { private screenshots = new Set(); + private failing = false; private success = true; public onTestEnd(test: TestCase): void { - if (!test.ok()) return; + if (!test.ok()) { + this.failing = true; + } for (const annotation of test.annotations) { if (annotation.type === "_screenshot") { this.screenshots.add(annotation.description); @@ -40,6 +43,7 @@ class StaleScreenshotReporter implements Reporter { } public async onExit(): Promise { + if (this.failing) return; const screenshotFiles = new Set(await glob(`**/*.png`, { cwd: snapshotRoot })); for (const screenshot of screenshotFiles) { if (screenshot.split("-").at(-1) !== "linux.png") { diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index d15f619dbc6..ba95129b66a 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -168,6 +168,14 @@ const DEFAULT_CONFIG = { access_token_ttl: 300, compat_token_ttl: 300, }, + rate_limiting: { + login: { + burst: 1000, + }, + registration: { + burst: 1000, + }, + }, }; export class MatrixAuthenticationServiceContainer extends GenericContainer { diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 5111a6f0a66..51658153610 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, RestartOptions, StartedTestContainer, Wait } from "testcontainers"; import { APIRequestContext } from "@playwright/test"; import crypto from "node:crypto"; import * as YAML from "yaml"; @@ -239,6 +239,11 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements super(container); } + public restart(options?: Partial): Promise { + this.adminToken = undefined; + return super.restart(options); + } + public setRequest(request: APIRequestContext): void { this.request = request; } From 7fbf9ef2699027c6c420f622533347ac0824e758 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 11:47:32 +0000 Subject: [PATCH 80/93] Fix mas config Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/mas.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index ba95129b66a..2c795c2c47d 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -170,10 +170,12 @@ const DEFAULT_CONFIG = { }, rate_limiting: { login: { - burst: 1000, + burst: 10, + per_second: 1, }, registration: { - burst: 1000, + burst: 10, + per_second: 1, }, }, }; From d27b55841f9ae7445b894f14733002297b267521 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 12:02:00 +0000 Subject: [PATCH 81/93] Fix another flaky test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/room-directory/room-directory.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright/e2e/room-directory/room-directory.spec.ts b/playwright/e2e/room-directory/room-directory.spec.ts index 34004c90d27..a38cc7d3950 100644 --- a/playwright/e2e/room-directory/room-directory.spec.ts +++ b/playwright/e2e/room-directory/room-directory.spec.ts @@ -30,6 +30,7 @@ test.describe("Room Directory", () => { // First add a local address `gaming` const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" }); await localAddresses.getByRole("textbox").fill("gaming"); + await expect(page.getByText("This address is available to use")).toBeVisible(); await localAddresses.getByRole("button", { name: "Add" }).click(); await expect(localAddresses.getByText("#gaming:localhost")).toHaveClass("mx_EditableItem_item"); From d8d3ebc6d644168804441a60e529a6e7dc841140 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 14:29:27 +0000 Subject: [PATCH 82/93] Fix playwright flakes due to floating promises Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintrc.js | 1 + playwright/e2e/accessibility/keyboard-navigation.spec.ts | 2 +- playwright/e2e/chat-export/html-export.spec.ts | 2 +- playwright/e2e/composer/RTE.spec.ts | 2 +- playwright/e2e/crypto/device-verification.spec.ts | 2 +- playwright/e2e/crypto/user-verification.spec.ts | 2 +- playwright/e2e/crypto/utils.ts | 4 ++-- playwright/e2e/knock/manage-knocks.spec.ts | 2 +- playwright/e2e/login/utils.ts | 2 +- playwright/e2e/polls/pollHistory.spec.ts | 2 +- playwright/e2e/room_options/marked_unread.spec.ts | 2 +- playwright/e2e/settings/account-user-settings-tab.spec.ts | 4 ++-- .../e2e/settings/preferences-user-settings-tab.spec.ts | 4 ++-- playwright/e2e/sliding-sync/sliding-sync.spec.ts | 4 ++-- playwright/e2e/threads/threads.spec.ts | 8 ++++---- playwright/e2e/widgets/stickers.spec.ts | 6 +++--- playwright/pages/bot.ts | 2 +- 17 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2b0dd2c186b..28d26696cb1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -271,6 +271,7 @@ module.exports = { }, rules: { "react-hooks/rules-of-hooks": ["off"], + "@typescript-eslint/no-floating-promises": ["error"], }, }, { diff --git a/playwright/e2e/accessibility/keyboard-navigation.spec.ts b/playwright/e2e/accessibility/keyboard-navigation.spec.ts index 6f4fc9be5fc..e22664c8985 100644 --- a/playwright/e2e/accessibility/keyboard-navigation.spec.ts +++ b/playwright/e2e/accessibility/keyboard-navigation.spec.ts @@ -123,7 +123,7 @@ test.describe("Landmark navigation tests", () => { await expect(page.getByText("Bob joined the room")).toBeVisible(); // Close the room - page.goto("/#/home"); + await page.goto("/#/home"); // Pressing Control+F6 will first focus the space button await page.keyboard.press("ControlOrMeta+F6"); diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts index 33ca6728c6c..760d3cc5f1c 100644 --- a/playwright/e2e/chat-export/html-export.spec.ts +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -95,7 +95,7 @@ test.describe("HTML Export", () => { async ({ page, app, room }) => { // Set a fixed time rather than masking off the line with the time in it: we don't need to worry // about the width changing and we can actually test this line looks correct. - page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); + await page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); // Send a bunch of messages to populate the room for (let i = 1; i < 10; i++) { diff --git a/playwright/e2e/composer/RTE.spec.ts b/playwright/e2e/composer/RTE.spec.ts index 3b750000c56..e88dd827fcc 100644 --- a/playwright/e2e/composer/RTE.spec.ts +++ b/playwright/e2e/composer/RTE.spec.ts @@ -165,7 +165,7 @@ test.describe("Composer", () => { // Type another await page.locator("div[contenteditable=true]").pressSequentially("my message 1"); // Send message - page.locator("div[contenteditable=true]").press("Enter"); + await page.locator("div[contenteditable=true]").press("Enter"); // It was sent await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible(); }); diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index a028bfb70c2..df75ff5f77b 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -212,7 +212,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { /* on the bot side, wait for the verifier to exist ... */ const verifier = await awaitVerifier(botVerificationRequest); // ... confirm ... - botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify()); + void botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify()); // ... and then check the emoji match await doTwoWaySasVerification(page, verifier); diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts index 7d428ac0604..175c8d5fdfd 100644 --- a/playwright/e2e/crypto/user-verification.spec.ts +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -74,7 +74,7 @@ test.describe("User verification", () => { /* on the bot side, wait for the verifier to exist ... */ const botVerifier = await awaitVerifier(bobVerificationRequest); // ... confirm ... - botVerifier.evaluate((verifier) => verifier.verify()); + void botVerifier.evaluate((verifier) => verifier.verify()); // ... and then check the emoji match await doTwoWaySasVerification(page, botVerifier); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 48da798f1a7..697572faa75 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -59,7 +59,7 @@ export function handleSasVerification(verifier: JSHandle): Promise((resolve) => { const onShowSas = (event: ShowSasCallbacks) => { verifier.off("show_sas" as VerifierEvent, onShowSas); - event.confirm(); + void event.confirm(); resolve(event.sas.emoji); }; @@ -313,7 +313,7 @@ export async function autoJoin(client: Client) { await client.evaluate((cli) => { cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => { if (member.membership === "invite" && member.userId === cli.getUserId()) { - cli.joinRoom(member.roomId); + void cli.joinRoom(member.roomId); } }); }); diff --git a/playwright/e2e/knock/manage-knocks.spec.ts b/playwright/e2e/knock/manage-knocks.spec.ts index fb7e2751945..6d0340170e1 100644 --- a/playwright/e2e/knock/manage-knocks.spec.ts +++ b/playwright/e2e/knock/manage-knocks.spec.ts @@ -50,7 +50,7 @@ test.describe("Manage Knocks", () => { }); test("should deny knock using bar", async ({ page, app, bot, room }) => { - bot.knockRoom(room.roomId); + await bot.knockRoom(room.roomId); const roomKnocksBar = page.locator(".mx_RoomKnocksBar"); await expect(roomKnocksBar.getByRole("heading", { name: "Asking to join" })).toBeVisible(); diff --git a/playwright/e2e/login/utils.ts b/playwright/e2e/login/utils.ts index cc98d8819a2..6e52dd99818 100644 --- a/playwright/e2e/login/utils.ts +++ b/playwright/e2e/login/utils.ts @@ -92,7 +92,7 @@ export async function interceptRequestsWithSoftLogout(page: Page, user: Credenti // do something to make the active /sync return: create a new room await page.evaluate(() => { // don't wait for this to complete: it probably won't, because of the broken sync - window.mxMatrixClientPeg.get().createRoom({}); + void window.mxMatrixClientPeg.get().createRoom({}); }); await promise; diff --git a/playwright/e2e/polls/pollHistory.spec.ts b/playwright/e2e/polls/pollHistory.spec.ts index a4d6a8ae0e0..319a08cda91 100644 --- a/playwright/e2e/polls/pollHistory.spec.ts +++ b/playwright/e2e/polls/pollHistory.spec.ts @@ -134,7 +134,7 @@ test.describe("Poll history", () => { await expect(dialog.getByText(pollParams2.title)).toBeAttached(); await expect(dialog.getByText(pollParams1.title)).toBeAttached(); - dialog.getByText("Active polls").click(); + await dialog.getByText("Active polls").click(); // no more active polls await expect(page.getByText("There are no active polls in this room")).toBeAttached(); diff --git a/playwright/e2e/room_options/marked_unread.spec.ts b/playwright/e2e/room_options/marked_unread.spec.ts index b314152e684..d7011684a7e 100644 --- a/playwright/e2e/room_options/marked_unread.spec.ts +++ b/playwright/e2e/room_options/marked_unread.spec.ts @@ -48,6 +48,6 @@ test.describe("Mark as Unread", () => { await roomTile.getByRole("button", { name: "Room options" }).click(); await page.getByRole("menuitem", { name: "Mark as unread" }).click(); - expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible(); + await expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible(); }); }); diff --git a/playwright/e2e/settings/account-user-settings-tab.spec.ts b/playwright/e2e/settings/account-user-settings-tab.spec.ts index f0374174477..df011bdc4e5 100644 --- a/playwright/e2e/settings/account-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/account-user-settings-tab.spec.ts @@ -34,14 +34,14 @@ test.describe("Account user settings tab", () => { await expect(profile.getByRole("textbox", { name: "Display Name" })).toHaveValue(USER_NAME); // Assert that a userId is rendered - expect(uut.getByLabel("Username")).toHaveText(user.userId); + await expect(uut.getByLabel("Username")).toHaveText(user.userId); // Wait until spinners disappear await expect(uut.getByTestId("accountSection").locator(".mx_Spinner")).not.toBeVisible(); await expect(uut.getByTestId("discoverySection").locator(".mx_Spinner")).not.toBeVisible(); const accountSection = uut.getByTestId("accountSection"); - accountSection.scrollIntoViewIfNeeded(); + await accountSection.scrollIntoViewIfNeeded(); // Assert that input areas for changing a password exists await expect(accountSection.getByLabel("Current password")).toBeVisible(); await expect(accountSection.getByLabel("New Password")).toBeVisible(); diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 4b6e3e299d7..5c7c9efffb8 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -24,7 +24,7 @@ test.describe("Preferences user settings tab", () => { }); test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => { - page.setViewportSize({ width: 1024, height: 3300 }); + await page.setViewportSize({ width: 1024, height: 3300 }); const tab = await app.settings.openUserSettings("Preferences"); // Assert that the top heading is rendered await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible(); @@ -61,7 +61,7 @@ test.describe("Preferences user settings tab", () => { // Click the button to display the dropdown menu await timezoneInput.getByRole("button", { name: "Set timezone" }).click(); // Select a different value - timezoneInput.getByRole("option", { name: /Africa\/Abidjan/ }).click(); + await timezoneInput.getByRole("option", { name: /Africa\/Abidjan/ }).click(); // Check the new value await expect(timezoneValue.getByText("Africa/Abidjan")).toBeVisible(); }); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 1ab7909a478..0fde7ddefb8 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -275,7 +275,7 @@ test.describe("Sliding Sync", () => { // now rescind the invite await bot.evaluate( async (client, { roomRescind, clientUserId }) => { - client.kick(roomRescind, clientUserId); + await client.kick(roomRescind, clientUserId); }, { roomRescind, clientUserId }, ); @@ -294,7 +294,7 @@ test.describe("Sliding Sync", () => { is_direct: true, }); await app.client.evaluate(async (client, roomId) => { - client.setRoomTag(roomId, "m.favourite", { order: 0.5 }); + await client.setRoomTag(roomId, "m.favourite", { order: 0.5 }); }, roomId); await expect(page.getByRole("group", { name: "Favourites" }).getByText("Favourite DM")).toBeVisible(); await expect(page.getByRole("group", { name: "People" }).getByText("Favourite DM")).not.toBeAttached(); diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index edcc0578d8a..4e84e812e2c 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -164,7 +164,7 @@ test.describe("Threads", () => { locator = page.locator( ".mx_ThreadView .mx_GenericEventListSummary[data-layout=bubble] .mx_EventTile_info.mx_EventTile_last", ); - expect(locator.locator(".mx_EventTile_line .mx_EventTile_content")) + await expect(locator.locator(".mx_EventTile_line .mx_EventTile_content")) // 76px: ThreadViewGroupSpacingStart + 14px + 6px // 14px: avatar width // See: _EventTile.pcss @@ -233,8 +233,8 @@ test.describe("Threads", () => { // User closes right panel after clicking back to thread list locator = page.locator(".mx_ThreadPanel"); - locator.getByRole("button", { name: "Threads" }).click(); - locator.getByRole("button", { name: "Close" }).click(); + await locator.getByRole("button", { name: "Threads" }).click(); + await locator.getByRole("button", { name: "Close" }).click(); // Bot responds to thread await bot.sendMessage(roomId, "How are things?", threadId); @@ -344,7 +344,7 @@ test.describe("Threads", () => { await expect(page.locator(".mx_ThreadView_timelinePanelWrapper")).toHaveCount(1); - (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Voice Message" }).click(); + await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Voice Message" }).click(); await page.waitForTimeout(3000); await app.getComposer(true).getByRole("button", { name: "Send voice message" }).click(); await expect(page.locator(".mx_ThreadView .mx_MVoiceMessageBody")).toHaveCount(1); diff --git a/playwright/e2e/widgets/stickers.spec.ts b/playwright/e2e/widgets/stickers.spec.ts index 54de1b69e28..2477efdc6b3 100644 --- a/playwright/e2e/widgets/stickers.spec.ts +++ b/playwright/e2e/widgets/stickers.spec.ts @@ -150,7 +150,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); const widgetHtml = getWidgetHtml(contentUri, "image/png"); stickerPickerUrl = webserver.start(widgetHtml); - setWidgetAccountData(app, user, stickerPickerUrl); + await setWidgetAccountData(app, user, stickerPickerUrl); await app.viewRoomByName(ROOM_NAME_1); await expect(page).toHaveURL(`/#/room/${room.roomId}`); @@ -177,7 +177,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); const widgetHtml = getWidgetHtml(contentUri, "image/png"); stickerPickerUrl = webserver.start(widgetHtml); - setWidgetAccountData(app, user, stickerPickerUrl, false); + await setWidgetAccountData(app, user, stickerPickerUrl, false); await app.viewRoomByName(ROOM_NAME_1); await expect(page).toHaveURL(`/#/room/${room.roomId}`); @@ -192,7 +192,7 @@ test.describe("Stickers", { tag: ["@no-firefox", "@no-webkit"] }, () => { }); const widgetHtml = getWidgetHtml(contentUri, "application/octet-stream"); stickerPickerUrl = webserver.start(widgetHtml); - setWidgetAccountData(app, user, stickerPickerUrl); + await setWidgetAccountData(app, user, stickerPickerUrl); await app.viewRoomByName(ROOM_NAME_1); await expect(page).toHaveURL(`/#/room/${room.roomId}`); diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index 1d414c7bf6a..200f83e2f6f 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -171,7 +171,7 @@ export class Bot extends Client { if (opts.autoAcceptInvites) { cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => { if (member.membership === "invite" && member.userId === cli.getUserId()) { - cli.joinRoom(member.roomId); + void cli.joinRoom(member.roomId); } }); } From 0124584caa463e18b39b43088d7989bbe50e8f5b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 14:46:11 +0000 Subject: [PATCH 83/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../synapse/legacyOAuthHomeserver.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts index 90737f94a4a..5e17d73b609 100644 --- a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -14,12 +14,23 @@ import { OAuthServer } from "../../oauth_server"; export const legacyOAuthHomeserver: Fixtures = { _oAuthServer: [ - async ({ _homeserver: container }, use) => { + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { const server = new OAuthServer(); + await use(server); + server.stop(); + }, + { scope: "worker" }, + ], + oAuthServer: async ({ _oAuthServer }, use, testInfo) => { + _oAuthServer.onTestStarted(testInfo); + await use(_oAuthServer); + }, + _homeserver: [ + async ({ _oAuthServer: server, _homeserver: homeserver }, use) => { const port = server.start(); - await TestContainers.exposeHostPorts(port); - container.withConfig({ + homeserver.withConfig({ oidc_providers: [ { idp_id: "test", @@ -43,13 +54,9 @@ export const legacyOAuthHomeserver: Fixtures { - _oAuthServer.onTestStarted(testInfo); - await use(_oAuthServer); - }, }; From 2cd40884a1aa940006c49a47ffb3a7aa9bad6e48 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 15:11:34 +0000 Subject: [PATCH 84/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../synapse/legacyOAuthHomeserver.ts | 20 +++++++++++-------- playwright/services.ts | 5 +---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts index 5e17d73b609..361a36a82e3 100644 --- a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -6,14 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { Fixtures } from "@playwright/test"; +import { Fixtures, PlaywrightTestArgs } from "@playwright/test"; import { TestContainers } from "testcontainers"; import { Services, TestFixtures } from "../../../services.ts"; import { OAuthServer } from "../../oauth_server"; -export const legacyOAuthHomeserver: Fixtures = { - _oAuthServer: [ +export const legacyOAuthHomeserver: Fixtures< + TestFixtures & PlaywrightTestArgs, + Services, + TestFixtures & PlaywrightTestArgs +> = { + oAuthServer: [ // eslint-disable-next-line no-empty-pattern async ({}, use) => { const server = new OAuthServer(); @@ -22,13 +26,13 @@ export const legacyOAuthHomeserver: Fixtures { - _oAuthServer.onTestStarted(testInfo); - await use(_oAuthServer); + context: async ({ context, oAuthServer }, use, testInfo) => { + oAuthServer.onTestStarted(testInfo); + await use(context); }, _homeserver: [ - async ({ _oAuthServer: server, _homeserver: homeserver }, use) => { - const port = server.start(); + async ({ oAuthServer, _homeserver: homeserver }, use) => { + const port = oAuthServer.start(); await TestContainers.exposeHostPorts(port); homeserver.withConfig({ oidc_providers: [ diff --git a/playwright/services.ts b/playwright/services.ts index 8f2ebb51d1e..5dae9b8a385 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -19,9 +19,6 @@ import { OAuthServer } from "./plugins/oauth_server"; export interface TestFixtures { mailhogClient: mailhog.API; - - // Set in legacyOAuthHomeserver only - oAuthServer?: OAuthServer; } export interface Services { @@ -37,7 +34,7 @@ export interface Services { mas?: StartedMatrixAuthenticationServiceContainer; // Set in legacyOAuthHomeserver only - _oAuthServer?: OAuthServer; + oAuthServer?: OAuthServer; } export const test = base.extend({ From 26e1a9bf7f1f7c1374d5d64bfedce2849ae1aad3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 17:05:47 +0000 Subject: [PATCH 85/93] Fix flaky playwright tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/crypto/backups-mas.spec.ts | 17 +++++++---- playwright/e2e/login/login-sso.spec.ts | 4 +-- .../e2e/login/soft_logout_oauth.spec.ts | 4 +-- playwright/e2e/login/utils.ts | 5 ++-- .../synapse/legacyOAuthHomeserver.ts | 29 +++++++++++++------ playwright/plugins/oauth_server/index.ts | 13 +++++++-- playwright/services.ts | 4 +++ 7 files changed, 53 insertions(+), 23 deletions(-) diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts index 614bde50646..1838f9e234b 100644 --- a/playwright/e2e/crypto/backups-mas.spec.ts +++ b/playwright/e2e/crypto/backups-mas.spec.ts @@ -19,19 +19,19 @@ test.use(masHomeserver); test.describe("Encryption state after registration", () => { test.skip(isDendrite, "does not yet support MAS"); - test("Key backup is enabled by default", async ({ page, mailhogClient, app }) => { + test("Key backup is enabled by default", async ({ page, mailhogClient, app }, testInfo) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!"); await app.settings.openUserSettings("Security & Privacy"); await expect(page.getByText("This session is backing up your keys.")).toBeVisible(); }); - test("user is prompted to set up recovery", async ({ page, mailhogClient, app }) => { + test("user is prompted to set up recovery", async ({ page, mailhogClient, app }, testInfo) => { await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); - await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!"); + await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!"); await page.getByRole("button", { name: "Add room" }).click(); await page.getByRole("menuitem", { name: "New room" }).click(); @@ -45,8 +45,13 @@ test.describe("Encryption state after registration", () => { test.describe("Key backup reset from elsewhere", () => { test.skip(isDendrite, "does not yet support MAS"); - test("Key backup is disabled when reset from elsewhere", async ({ page, mailhogClient, request, homeserver }) => { - const testUsername = "alice"; + test("Key backup is disabled when reset from elsewhere", async ({ + page, + mailhogClient, + request, + homeserver, + }, testInfo) => { + const testUsername = `alice_${testInfo.testId}`; const testPassword = "Pa$sW0rD!"; // there's a delay before keys are uploaded so the error doesn't appear immediately: use a fake diff --git a/playwright/e2e/login/login-sso.spec.ts b/playwright/e2e/login/login-sso.spec.ts index fbe190b9358..22428af5028 100644 --- a/playwright/e2e/login/login-sso.spec.ts +++ b/playwright/e2e/login/login-sso.spec.ts @@ -17,13 +17,13 @@ test.use(legacyOAuthHomeserver); test.describe("SSO login", () => { test.skip(isDendrite, "does not yet support SSO"); - test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => { + test("logs in with SSO and lands on the home screen", async ({ page, homeserver }, testInfo) => { // If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to // your firewall settings: Synapse is unable to reach the OIDC server. // // If you are using ufw, try something like: // sudo ufw allow in on docker0 // - await doTokenRegistration(page, homeserver); + await doTokenRegistration(page, homeserver, testInfo); }); }); diff --git a/playwright/e2e/login/soft_logout_oauth.spec.ts b/playwright/e2e/login/soft_logout_oauth.spec.ts index 19b1fc0124c..f6814d0cf4a 100644 --- a/playwright/e2e/login/soft_logout_oauth.spec.ts +++ b/playwright/e2e/login/soft_logout_oauth.spec.ts @@ -26,8 +26,8 @@ test.use({ test.use(legacyOAuthHomeserver); test.describe("Soft logout with SSO user", () => { test.use({ - user: async ({ page, homeserver }, use) => { - const user = await doTokenRegistration(page, homeserver); + user: async ({ page, homeserver }, use, testInfo) => { + const user = await doTokenRegistration(page, homeserver, testInfo); // Eventually, we should end up at the home screen. await expect(page).toHaveURL(/\/#\/home$/); diff --git a/playwright/e2e/login/utils.ts b/playwright/e2e/login/utils.ts index cc98d8819a2..e7121159f0f 100644 --- a/playwright/e2e/login/utils.ts +++ b/playwright/e2e/login/utils.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { Page, expect } from "@playwright/test"; +import { Page, expect, TestInfo } from "@playwright/test"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; @@ -15,6 +15,7 @@ import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; export async function doTokenRegistration( page: Page, homeserver: HomeserverInstance, + testInfo: TestInfo, ): Promise { await page.goto("/#/login"); @@ -35,7 +36,7 @@ export async function doTokenRegistration( // Synapse prompts us to pick a user ID await expect(page.getByRole("heading", { name: "Create your account" })).toBeVisible(); - await page.getByRole("textbox", { name: "Username (required)" }).fill("alice"); + await page.getByRole("textbox", { name: "Username (required)" }).fill(`alice_${testInfo.testId}`); // wait for username validation to start, and complete await expect(page.locator("#field-username-output")).toHaveText(""); diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts index 246829e422d..2e3d20c9c0d 100644 --- a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -6,20 +6,31 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { Fixtures } from "@playwright/test"; +import { Fixtures, PlaywrightTestArgs } from "@playwright/test"; import { TestContainers } from "testcontainers"; import { Services } from "../../../services.ts"; import { OAuthServer } from "../../oauth_server"; -export const legacyOAuthHomeserver: Fixtures<{}, Services> = { - _homeserver: [ - async ({ _homeserver: container }, use) => { +export const legacyOAuthHomeserver: Fixtures = { + oAuthServer: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { const server = new OAuthServer(); - const port = server.start(); - + await use(server); + server.stop(); + }, + { scope: "worker" }, + ], + context: async ({ context, oAuthServer }, use, testInfo) => { + oAuthServer.onTestStarted(testInfo); + await use(context); + }, + _homeserver: [ + async ({ oAuthServer, _homeserver: homeserver }, use) => { + const port = oAuthServer.start(); await TestContainers.exposeHostPorts(port); - container.withConfig({ + homeserver.withConfig({ oidc_providers: [ { idp_id: "test", @@ -43,8 +54,8 @@ export const legacyOAuthHomeserver: Fixtures<{}, Services> = { }, ], }); - await use(container); - server.stop(); + + await use(homeserver); }, { scope: "worker" }, ], diff --git a/playwright/plugins/oauth_server/index.ts b/playwright/plugins/oauth_server/index.ts index 3f80dc11ca9..df5ee0f461b 100644 --- a/playwright/plugins/oauth_server/index.ts +++ b/playwright/plugins/oauth_server/index.ts @@ -9,12 +9,21 @@ Please see LICENSE files in the repository root for full details. import http from "http"; import express from "express"; import { AddressInfo } from "net"; +import { TestInfo } from "@playwright/test"; + +import { randB64Bytes } from "../utils/rand.ts"; export class OAuthServer { private server?: http.Server; + private sub?: string; + + public onTestStarted(testInfo: TestInfo): void { + this.sub = testInfo.testId; + } public start(): number { if (this.server) this.stop(); + const token = randB64Bytes(16); const app = express(); @@ -28,7 +37,7 @@ export class OAuthServer { const code = req.body.code; if (code === "valid_auth_code") { res.send({ - access_token: "oauth_access_token", + access_token: token, token_type: "Bearer", expires_in: "3600", }); @@ -43,7 +52,7 @@ export class OAuthServer { // return an OAuth2 user info object res.send({ - sub: "alice", + sub: this.sub, name: "Alice", }); }); diff --git a/playwright/services.ts b/playwright/services.ts index 213af7df426..50cd49ed9bf 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -15,6 +15,7 @@ import { Logger } from "./logger.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts"; import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailhog.ts"; +import { OAuthServer } from "./plugins/oauth_server"; interface TestFixtures { mailhogClient: mailhog.API; @@ -30,7 +31,10 @@ export interface Services { synapseConfigOptions: SynapseConfigOptions; _homeserver: HomeserverContainer; homeserver: StartedHomeserverContainer; + // Set in masHomeserver only mas?: StartedMatrixAuthenticationServiceContainer; + // Set in legacyOAuthHomeserver only + oAuthServer?: OAuthServer; } export const test = base.extend({ From 0e7c40c79255fa8bd0f779b0481e02a01713bc11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 17:47:11 +0000 Subject: [PATCH 86/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/synapse.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 669e2ccd0a5..e6744b340a3 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -244,7 +244,6 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { protected adminTokenPromise?: Promise; - protected _request?: APIRequestContext; protected readonly adminApi: Api; public readonly csApi: ClientServerApi; @@ -263,13 +262,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements return super.restart(options); } - public restart(options?: Partial): Promise { - this.adminToken = undefined; - return super.restart(options); - } - public setRequest(request: APIRequestContext): void { - this._request = request; this.csApi.setRequest(request); this.adminApi.setRequest(request); } From cae93faec6c45a50c09223fd01d97f988b066148 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 17:50:28 +0000 Subject: [PATCH 87/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/csAPI.ts | 2 +- playwright/plugins/homeserver/index.ts | 2 +- playwright/{testcontainers/utils.ts => plugins/utils/api.ts} | 4 ++-- playwright/testcontainers/synapse.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename playwright/{testcontainers/utils.ts => plugins/utils/api.ts} (96%) diff --git a/playwright/e2e/csAPI.ts b/playwright/e2e/csAPI.ts index f171ded5e3b..4153d09199c 100644 --- a/playwright/e2e/csAPI.ts +++ b/playwright/e2e/csAPI.ts @@ -9,7 +9,7 @@ import { APIRequestContext } from "playwright-core"; import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { HomeserverInstance } from "../plugins/homeserver"; -import { ClientServerApi } from "../testcontainers/utils.ts"; +import { ClientServerApi } from "../plugins/utils/api.ts"; /** * A small subset of the Client-Server API used to manipulate the state of the diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index 9e54e0aa916..b6956a82678 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { ClientServerApi } from "../../testcontainers/utils.ts"; +import { ClientServerApi } from "../utils/api.ts"; export interface HomeserverInstance { readonly baseUrl: string; diff --git a/playwright/testcontainers/utils.ts b/playwright/plugins/utils/api.ts similarity index 96% rename from playwright/testcontainers/utils.ts rename to playwright/plugins/utils/api.ts index cfe538edfc1..ccee723687f 100644 --- a/playwright/testcontainers/utils.ts +++ b/playwright/plugins/utils/api.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { APIRequestContext } from "@playwright/test"; -import { Credentials } from "../plugins/homeserver"; +import { Credentials } from "../homeserver"; export type Verb = "GET" | "POST" | "PUT" | "DELETE"; diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index e6744b340a3..5460c9300ba 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -17,7 +17,7 @@ import { Credentials } from "../plugins/homeserver"; import { deepCopy } from "../plugins/utils/object.ts"; import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverContainer.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; -import { Api, ClientServerApi, Verb } from "./utils.ts"; +import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts"; const TAG = "develop@sha256:b69222d98abe9625d46f5d3cb01683d5dc173ae339215297138392cfeec935d9"; From f02967e9153af63870835c0edd566c20649c95fe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 17:56:00 +0000 Subject: [PATCH 88/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts | 2 +- playwright/services.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts index 06992581193..7414fdb0152 100644 --- a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -21,7 +21,7 @@ export const legacyOAuthHomeserver: Fixtures = { }, { scope: "worker" }, ], - context: async ({ context, oAuthServer }, use, testInfo) => { + context: async ({ homeserverType, context, oAuthServer }, use, testInfo) => { testInfo.skip(homeserverType !== "synapse", "does not yet support OIDC"); oAuthServer.onTestStarted(testInfo); await use(context); diff --git a/playwright/services.ts b/playwright/services.ts index f4fbbc2c113..949882199c5 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -156,7 +156,7 @@ export const test = base.extend({ { scope: "worker" }, ], - context: async ({ homeserverType, synapseConfigOptions, logger, context, request, homeserver, mailhogClient }, use, testInfo) => { + context: async ({ homeserverType, synapseConfigOptions, logger, context, request, homeserver }, use, testInfo) => { testInfo.skip( !(homeserver instanceof SynapseContainer) && Object.keys(synapseConfigOptions).length > 0, `Test specifies Synapse config options so is unsupported with ${homeserverType}`, From 504f352ef6ff591e33a6867303ed21a169166eec Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 18:35:52 +0000 Subject: [PATCH 89/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/e2e/read-receipts/reactions-in-threads.spec.ts | 6 +----- playwright/testcontainers/dendrite.ts | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts index b88e18afd8b..b2cd2e554aa 100644 --- a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts @@ -73,11 +73,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => { // Given a thread exists and I have marked it as read await util.goTo(room1); await util.assertRead(room2); - await util.receiveMessages(room2, [ - "Msg1", - msg.threadedOff("Msg1", "Reply1"), - msg.reactionTo("Reply1", "🪿"), - ]); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); await util.assertUnread(room2, 1); await util.markAsRead(room2); await util.assertRead(room2); diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index c358ff15852..fb0fbf93758 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -237,7 +237,8 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon } public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this { - throw new Error("Dendrite does not support MAS."); + if (mas) throw new Error("Dendrite does not support MAS."); + return this; } public override async start(): Promise { From 2356459272e507cefcf782c8467036fa32afffe1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 19:01:33 +0000 Subject: [PATCH 90/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/testcontainers/dendrite.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index fb0fbf93758..58ab844a7ca 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -236,8 +236,8 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon return this; } + // Dendrite does not support MAS at this time public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this { - if (mas) throw new Error("Dendrite does not support MAS."); return this; } @@ -265,4 +265,10 @@ export class PineconeContainer extends DendriteContainer { } // Surprisingly, Dendrite implements the same register user Synapse Admin API, so we can just extend it -export class StartedDendriteContainer extends StartedSynapseContainer {} +export class StartedDendriteContainer extends StartedSynapseContainer { + protected async deletePublicRooms(): Promise { + // Dendrite does not support admin users managing the room directory + // https://github.com/element-hq/dendrite/blob/main/clientapi/routing/directory.go#L365 + return; + } +} From 53b2e30a8b92be5d5dccb523e41012ce68c3d167 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jan 2025 19:18:32 +0000 Subject: [PATCH 91/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- playwright/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/services.ts b/playwright/services.ts index 949882199c5..b4c86bc72a6 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -103,8 +103,8 @@ export const test = base.extend({ { scope: "worker" }, ], mailhogClient: async ({ mailhog: container }, use) => { - await use(container.client); await container.client.deleteAll(); + await use(container.client); }, synapseConfigOptions: [{}, { option: true, scope: "worker" }], From 84bc096c274778d8dfe6db186e4047141bc6c686 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Jan 2025 11:19:18 +0000 Subject: [PATCH 92/93] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- docs/playwright.md | 4 ++-- playwright/services.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/playwright.md b/docs/playwright.md index 2c26b7ab2be..e03d1f5f8da 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -66,11 +66,11 @@ as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`. of Synapse/Dendrite. These servers are what Element-web runs against in the tests. Synapse can be launched with different configurations in order to test element -in different configurations. You can specify `synapseConfigOptions` as such: +in different configurations. You can specify `synapseConfig` as such: ```typescript test.use({ - synapseConfigOptions: { + synapseConfig: { // The config options to pass to the Synapse instance }, }); diff --git a/playwright/services.ts b/playwright/services.ts index d9c4f6d36bd..ba5baabf532 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -30,7 +30,6 @@ export interface Services { postgres: StartedPostgreSqlContainer; mailhog: StartedMailhogContainer; - synapseConfig: SynapseConfig; _homeserver: HomeserverContainer; homeserver: StartedHomeserverContainer; // Set in masHomeserver only @@ -40,7 +39,7 @@ export interface Services { } export interface Options { - synapseConfigOptions: SynapseConfigOptions; + synapseConfig: SynapseConfig; homeserverType: HomeserverType; } @@ -156,9 +155,9 @@ export const test = base.extend({ { scope: "worker" }, ], - context: async ({ homeserverType, synapseConfigOptions, logger, context, request, homeserver }, use, testInfo) => { + context: async ({ homeserverType, synapseConfig, logger, context, request, homeserver }, use, testInfo) => { testInfo.skip( - !(homeserver instanceof SynapseContainer) && Object.keys(synapseConfigOptions).length > 0, + !(homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0, `Test specifies Synapse config options so is unsupported with ${homeserverType}`, ); homeserver.setRequest(request); From a7ec37fb21d30ee66079c49ba5a77972fc8299ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Jan 2025 14:55:01 +0000 Subject: [PATCH 93/93] Update services.ts --- playwright/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/services.ts b/playwright/services.ts index ba5baabf532..c15d63bd025 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -30,6 +30,7 @@ export interface Services { postgres: StartedPostgreSqlContainer; mailhog: StartedMailhogContainer; + synapseConfig: SynapseConfig; _homeserver: HomeserverContainer; homeserver: StartedHomeserverContainer; // Set in masHomeserver only @@ -39,7 +40,6 @@ export interface Services { } export interface Options { - synapseConfig: SynapseConfig; homeserverType: HomeserverType; }