diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts new file mode 100644 index 0000000000..3d56534728 --- /dev/null +++ b/playwright/e2e/crypto/backups-mas.spec.ts @@ -0,0 +1,91 @@ +/* +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 OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test, expect } from "../../element-web-test"; +import { registerAccountMas } from "../oidc"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { TestClientServerAPI } from "../csAPI"; +import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; + +// 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 user and synthesizes an existing login, +// which is faster but leaves us without crypto set up. +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 }) => { + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await registerAccountMas(page, mailhogClient, "alice", "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 }) => { + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + 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(); + await page.getByRole("textbox", { name: "Name" }).fill("test room"); + await page.getByRole("button", { name: "Create room" }).click(); + + await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible(); + }); +}); + +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"; + 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(); + + 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(); + + // @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 backupInfo = await csAPI.getCurrentBackupInfo(); + + 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("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(); + + // 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(); + }); +}); diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index ff792f9785..95bf708122 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -9,10 +9,7 @@ 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 { registerAccountMas } from "../oidc"; import { isDendrite } from "../../plugins/homeserver/dendrite"; -import { TestClientServerAPI } from "../csAPI"; -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( @@ -22,83 +19,6 @@ async function expectBackupVersionToBe(page: Page, version: string) { await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version); } -// 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 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); - - 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!"); - - 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 }) => { - await page.goto("/#/login"); - await page.getByRole("button", { name: "Continue" }).click(); - 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(); - await page.getByRole("textbox", { name: "Name" }).fill("test room"); - await page.getByRole("button", { name: "Create room" }).click(); - - await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible(); - }); -}); - -test.describe("Key backup reset from elsewhere", () => { - test.use(masHomeserver); - - 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(); - - 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(); - - // @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 backupInfo = await csAPI.getCurrentBackupInfo(); - - 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("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(); - - // 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(); - }); -}); - test.describe("Backups", () => { test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.use({ diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 7d05d71ae1..7c1bbd7ac4 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -19,31 +19,31 @@ function getMemberTileByName(page: Page, name: string): Locator { return page.locator(`.mx_EntityTile, [title="${name}"]`); } +test.use({ + displayName: NAME, + synapseConfigOptions: { + experimental_features: { + msc2697_enabled: false, + msc3814_enabled: true, + }, + }, + config: async ({ config, context }, use) => { + const wellKnown = { + ...config.default_server_config, + "org.matrix.msc3814": true, + }; + + await context.route("https://localhost/.well-known/matrix/client", async (route) => { + await route.fulfill({ json: wellKnown }); + }); + + await use(config); + }, +}); + test.describe("Dehydration", () => { test.skip(isDendrite, "does not yet support dehydration v2"); - test.use({ - displayName: NAME, - synapseConfigOptions: { - experimental_features: { - msc2697_enabled: false, - msc3814_enabled: true, - }, - }, - config: async ({ config, context }, use) => { - const wellKnown = { - ...config.default_server_config, - "org.matrix.msc3814": true, - }; - - await context.route("https://localhost/.well-known/matrix/client", async (route) => { - await route.fulfill({ json: wellKnown }); - }); - - await use(config); - }, - }); - test("Create dehydrated device", async ({ page, user, app }, workerInfo) => { // Create a backup (which will create SSSS, and dehydrated device) diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index 621cf3fd3c..71475e892e 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -16,20 +16,21 @@ const username = "user1234"; const password = "oETo7MPf0o"; const email = "user@nowhere.dummy"; -test.describe("Forgot Password", () => { - test.skip(isDendrite, "not yet wired up"); - test.use(emailHomeserver); - 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.use(emailHomeserver); +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("Forgot Password", () => { + test.skip(isDendrite, "not yet wired up"); test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { await page.goto("/"); diff --git a/playwright/e2e/login/consent.spec.ts b/playwright/e2e/login/consent.spec.ts index 9127ae54bb..aab190298f 100644 --- a/playwright/e2e/login/consent.spec.ts +++ b/playwright/e2e/login/consent.spec.ts @@ -9,12 +9,12 @@ 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({ - displayName: "Bob", - }); +test.use(consentHomeserver); +test.use({ + displayName: "Bob", +}); +test.describe("Consent", () => { test("should prompt the user to consent to terms when server deems it necessary", async ({ context, page, diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login-consent.spec.ts similarity index 69% rename from playwright/e2e/login/login.spec.ts rename to playwright/e2e/login/login-consent.spec.ts index fd879d77cf..32f0846351 100644 --- a/playwright/e2e/login/login.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -9,13 +9,12 @@ Please see LICENSE files in the repository root for full details. import { Page } from "playwright-core"; import { expect, test } from "../../element-web-test"; -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"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; +// This test requires fixed credentials for the device signing keys below to work const username = "user1234"; const password = "p4s5W0rD"; @@ -70,39 +69,44 @@ const DEVICE_SIGNING_KEYS_BODY = { }, }; -async function login(page: Page, homeserver: HomeserverInstance) { +async function login(page: Page, homeserver: HomeserverInstance, credentials: Credentials) { await page.getByRole("link", { name: "Sign in" }).click(); await selectHomeserver(page, homeserver.baseUrl); - await page.getByRole("textbox", { name: "Username" }).fill(username); - await page.getByPlaceholder("Password").fill(password); + await page.getByRole("textbox", { name: "Username" }).fill(credentials.username); + await page.getByPlaceholder("Password").fill(credentials.password); await page.getByRole("button", { name: "Sign in" }).click(); } -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.use(consentHomeserver); +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", }, }, - }); + }, + 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}`); + await use({ + ...credentials, + displayName, + }); + }, +}); + +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; - - test.beforeEach(async ({ homeserver }) => { - creds = await homeserver.registerUser(username, password); - }); test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({ + credentials, page, homeserver, checkA11y, @@ -136,16 +140,16 @@ test.describe("Login", () => { // cy.percySnapshot("Login"); await checkA11y(); - await page.getByRole("textbox", { name: "Username" }).fill(username); - await page.getByPlaceholder("Password").fill(password); + await page.getByRole("textbox", { name: "Username" }).fill(credentials.username); + await page.getByPlaceholder("Password").fill(credentials.password); await page.getByRole("button", { name: "Sign in" }).click(); await expect(page).toHaveURL(/\/#\/home$/); }); - test("Follows the original link after login", async ({ page, homeserver }) => { + test("Follows the original link after login", async ({ page, homeserver, credentials }) => { await page.goto("/#/room/!room:id"); // should redirect to the welcome page - await login(page, homeserver); + await login(page, homeserver, credentials); await expect(page).toHaveURL(/\/#\/room\/!room:id$/); await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible(); @@ -156,9 +160,10 @@ test.describe("Login", () => { page, homeserver, request, + credentials, }) => { const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { - headers: { Authorization: `Bearer ${creds.accessToken}` }, + headers: { Authorization: `Bearer ${credentials.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY, }); if (res.status() / 100 !== 2) { @@ -167,7 +172,7 @@ test.describe("Login", () => { expect(res.status() / 100).toEqual(2); await page.goto("/"); - await login(page, homeserver); + await login(page, homeserver, credentials); await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible(); @@ -185,10 +190,14 @@ test.describe("Login", () => { page, homeserver, request, + credentials, }) => { const res = await request.post( `${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, - { headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY }, + { + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + data: DEVICE_SIGNING_KEYS_BODY, + }, ); if (res.status() / 100 !== 2) { console.log("Uploading dummy keys failed", await res.json()); @@ -196,7 +205,7 @@ test.describe("Login", () => { expect(res.status() / 100).toEqual(2); await page.goto("/"); - await login(page, homeserver); + await login(page, homeserver, credentials); await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible(); @@ -215,11 +224,15 @@ test.describe("Login", () => { page, homeserver, request, + credentials, }) => { - console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY); + console.log(`uid ${credentials.userId} body`, 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 }, + { + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + data: DEVICE_SIGNING_KEYS_BODY, + }, ); if (res.status() / 100 !== 2) { console.log("Uploading dummy keys failed", await res.json()); @@ -227,9 +240,9 @@ test.describe("Login", () => { expect(res.status() / 100).toEqual(2); await page.goto("/"); - await login(page, homeserver); + await login(page, homeserver, credentials); - const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 }); + const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 }); await expect(h1).toBeVisible(); await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0); @@ -238,25 +251,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(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 - // 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); - }); - }); - test.describe("logout", () => { - test.use(consentHomeserver); - test("should go to login page on logout", async ({ page, user }) => { await page.getByRole("button", { name: "User menu" }).click(); await expect(page.getByText(user.displayName, { exact: true })).toBeVisible(); @@ -268,29 +263,4 @@ test.describe("Login", () => { await expect(page).toHaveURL(/\/#\/login$/); }); }); - - test.describe("logout with logout_redirect_url", () => { - test.use(consentHomeserver); - test.use({ - 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 - // infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a - // `test-landing.html` page when running with an uncontrolled Element (via `yarn start`). - // Using the decoder-ring is just as fine, and we can search for strategic names. - logout_redirect_url: "/decoder-ring/", - }, - }); - - test("should respect logout_redirect_url", async ({ page, user }) => { - await page.getByRole("button", { name: "User menu" }).click(); - await expect(page.getByText(user.displayName, { exact: true })).toBeVisible(); - - // give a change for the outstanding requests queue to settle before logging out - await page.waitForTimeout(2000); - - await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click(); - await expect(page).toHaveURL(/\/decoder-ring\/$/); - }); - }); }); diff --git a/playwright/e2e/login/login-sso.spec.ts b/playwright/e2e/login/login-sso.spec.ts new file mode 100644 index 0000000000..fbe190b935 --- /dev/null +++ b/playwright/e2e/login/login-sso.spec.ts @@ -0,0 +1,29 @@ +/* +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 { test } from "../../element-web-test"; +import { doTokenRegistration } from "./utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts"; + +test.use(legacyOAuthHomeserver); + +// 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("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 + // 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); + }); +}); diff --git a/playwright/e2e/login/logout_redirect_url.spec.ts b/playwright/e2e/login/logout_redirect_url.spec.ts new file mode 100644 index 0000000000..c27acf96be --- /dev/null +++ b/playwright/e2e/login/logout_redirect_url.spec.ts @@ -0,0 +1,35 @@ +/* +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 { expect, test } from "../../element-web-test"; +import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts"; + +test.use(consentHomeserver); +test.use({ + 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 + // infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a + // `test-landing.html` page when running with an uncontrolled Element (via `yarn start`). + // Using the decoder-ring is just as fine, and we can search for strategic names. + logout_redirect_url: "/decoder-ring/", + }, +}); + +test.describe("logout with logout_redirect_url", () => { + test("should respect logout_redirect_url", async ({ page, user }) => { + await page.getByRole("button", { name: "User menu" }).click(); + await expect(page.getByText(user.displayName, { exact: true })).toBeVisible(); + + // give a change for the outstanding requests queue to settle before logging out + await page.waitForTimeout(2000); + + await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click(); + await expect(page).toHaveURL(/\/decoder-ring\/$/); + }); +}); diff --git a/playwright/e2e/login/soft_logout.spec.ts b/playwright/e2e/login/soft_logout.spec.ts index 89060c4da4..2c9325e757 100644 --- a/playwright/e2e/login/soft_logout.spec.ts +++ b/playwright/e2e/login/soft_logout.spec.ts @@ -6,124 +6,38 @@ 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 } from "@playwright/test"; - 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"; +import { interceptRequestsWithSoftLogout } from "./utils"; -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", - }, +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", }, }, - }); - - test.describe("with password user", () => { - test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => { - await interceptRequestsWithSoftLogout(page, user); - await expect(page.getByText("You're signed out")).toBeVisible(); - await page.getByPlaceholder("Password").fill(user.password); - await page.getByPlaceholder("Password").press("Enter"); - - // back to the welcome page - await expect(page).toHaveURL(/\/#\/home/); - await expect( - page.getByRole("heading", { name: "Now, let's help you get started", exact: true }), - ).toBeVisible(); - }); - - test("still shows the soft-logout page when the page is reloaded after a soft-logout", async ({ - page, - user, - }) => { - await interceptRequestsWithSoftLogout(page, user); - await expect(page.getByText("You're signed out")).toBeVisible(); - await page.reload(); - await expect(page.getByText("You're signed out")).toBeVisible(); - }); - }); - - 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); - - // Eventually, we should end up at the home screen. - await expect(page).toHaveURL(/\/#\/home$/); - await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible(); - - await use(user); - }, - }); - - test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => { - await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible(); - - await interceptRequestsWithSoftLogout(page, user); - - await expect(page.getByText("You're signed out")).toBeVisible(); - await page.getByRole("button", { name: "Continue with OAuth test" }).click(); - - // click the submit button - await page.getByRole("button", { name: "Submit" }).click(); - - // Synapse prompts us to grant permission to Element - await expect(page.getByRole("heading", { name: "Continue to your account" })).toBeVisible(); - await page.getByRole("link", { name: "Continue" }).click(); - - // back to the welcome page - await expect(page).toHaveURL(/\/#\/home$/); - await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible(); - }); - }); + }, }); -/** - * Intercept calls to /sync and have them fail with a soft-logout - * - * Any further requests to /sync with the same access token are blocked. - */ -async function interceptRequestsWithSoftLogout(page: Page, user: Credentials): Promise { - await page.route("**/_matrix/client/*/sync*", async (route, req) => { - const accessToken = await req.headerValue("Authorization"); +test.describe("Soft logout with password user", () => { + test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => { + await interceptRequestsWithSoftLogout(page, user); + await expect(page.getByText("You're signed out")).toBeVisible(); + await page.getByPlaceholder("Password").fill(user.password); + await page.getByPlaceholder("Password").press("Enter"); - // now, if the access token on this request matches the expired one, block it - if (accessToken === `Bearer ${user.accessToken}`) { - console.log("Intercepting request with soft-logged-out access token"); - await route.fulfill({ - status: 401, - json: { - errcode: "M_UNKNOWN_TOKEN", - error: "Soft logout", - soft_logout: true, - }, - }); - return; - } - - // otherwise, pass through as normal - await route.continue(); + // back to the welcome page + await expect(page).toHaveURL(/\/#\/home/); + await expect(page.getByRole("heading", { name: "Now, let's help you get started", exact: true })).toBeVisible(); }); - const promise = page.waitForResponse((resp) => resp.url().includes("/sync") && resp.status() === 401); - - // 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({}); + test("still shows the soft-logout page when the page is reloaded after a soft-logout", async ({ page, user }) => { + await interceptRequestsWithSoftLogout(page, user); + await expect(page.getByText("You're signed out")).toBeVisible(); + await page.reload(); + await expect(page.getByText("You're signed out")).toBeVisible(); }); - - await promise; -} +}); diff --git a/playwright/e2e/login/soft_logout_oauth.spec.ts b/playwright/e2e/login/soft_logout_oauth.spec.ts new file mode 100644 index 0000000000..19b1fc0124 --- /dev/null +++ b/playwright/e2e/login/soft_logout_oauth.spec.ts @@ -0,0 +1,59 @@ +/* +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 OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test, expect } from "../../element-web-test"; +import { doTokenRegistration, interceptRequestsWithSoftLogout } from "./utils"; +import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts"; + +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", + }, + }, + }, +}); + +test.use(legacyOAuthHomeserver); +test.describe("Soft logout with SSO user", () => { + test.use({ + user: async ({ page, homeserver }, use) => { + const user = await doTokenRegistration(page, homeserver); + + // Eventually, we should end up at the home screen. + await expect(page).toHaveURL(/\/#\/home$/); + await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible(); + + await use(user); + }, + }); + + test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => { + await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible(); + + await interceptRequestsWithSoftLogout(page, user); + + await expect(page.getByText("You're signed out")).toBeVisible(); + await page.getByRole("button", { name: "Continue with OAuth test" }).click(); + + // click the submit button + await page.getByRole("button", { name: "Submit" }).click(); + + // Synapse prompts us to grant permission to Element + await expect(page.getByRole("heading", { name: "Continue to your account" })).toBeVisible(); + await page.getByRole("link", { name: "Continue" }).click(); + + // back to the welcome page + await expect(page).toHaveURL(/\/#\/home$/); + await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible(); + }); +}); diff --git a/playwright/e2e/login/utils.ts b/playwright/e2e/login/utils.ts index 0a728faecc..cc98d8819a 100644 --- a/playwright/e2e/login/utils.ts +++ b/playwright/e2e/login/utils.ts @@ -20,7 +20,7 @@ export async function doTokenRegistration( await page.getByRole("button", { name: "Edit" }).click(); await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl); - await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); @@ -56,5 +56,44 @@ export async function doTokenRegistration( homeServer: window.mxMatrixClientPeg.get().getHomeserverUrl(), password: null, displayName: "Alice", + username: window.mxMatrixClientPeg.get().getUserIdLocalpart(), })); } + +/** + * Intercept calls to /sync and have them fail with a soft-logout + * + * Any further requests to /sync with the same access token are blocked. + */ +export async function interceptRequestsWithSoftLogout(page: Page, user: Credentials): Promise { + await page.route("**/_matrix/client/*/sync*", async (route, req) => { + const accessToken = await req.headerValue("Authorization"); + + // now, if the access token on this request matches the expired one, block it + if (accessToken === `Bearer ${user.accessToken}`) { + console.log("Intercepting request with soft-logged-out access token"); + await route.fulfill({ + status: 401, + json: { + errcode: "M_UNKNOWN_TOKEN", + error: "Soft logout", + soft_logout: true, + }, + }); + return; + } + + // otherwise, pass through as normal + await route.continue(); + }); + + const promise = page.waitForResponse((resp) => resp.url().includes("/sync") && resp.status() === 401); + + // 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({}); + }); + + await promise; +} diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 7c3d7f84ff..3c7ffef4ac 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -11,8 +11,8 @@ import { registerAccountMas } from "."; import { ElementAppPage } from "../../pages/ElementAppPage.ts"; import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; +test.use(masHomeserver); test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { - test.use(masHomeserver); 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/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 50cfb4297a..74c8ba7962 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -10,21 +10,22 @@ import { test, expect } from "../../element-web-test"; import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +test.use(emailHomeserver); +test.use({ + config: ({ config }, use) => + use({ + ...config, + default_server_config: { + ...config.default_server_config, + "m.identity_server": { + base_url: "https://server.invalid", + }, + }, + }), +}); + test.describe("Email Registration", async () => { test.skip(isDendrite, "not yet wired up"); - test.use(emailHomeserver); - test.use({ - config: ({ config }, use) => - use({ - ...config, - default_server_config: { - ...config.default_server_config, - "m.identity_server": { - base_url: "https://server.invalid", - }, - }, - }), - }); 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 5c842a363f..a76900d8aa 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -10,20 +10,21 @@ 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.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); - test.use(consentHomeserver); - 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.use(consentHomeserver); +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("Registration", () => { + test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.beforeEach(async ({ page }) => { await page.goto("/#/register"); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 66a7318a04..2f32c82105 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -376,37 +376,42 @@ test.describe("Sliding Sync", () => { roomIds.push(id); await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible(); } - const [roomAId, roomPId] = roomIds; + const [roomAId, roomPId, roomOId] = roomIds; - const assertUnsubExists = (request: Request, subRoomId: string, unsubRoomId: string) => { + const matchRoomSubRequest = (subRoomId: string) => (request: Request) => { + if (!request.url().includes("/sync")) return false; const body = request.postDataJSON(); - // There may be a request without a txn_id, ignore it, as there won't be any subscription changes - if (body.txn_id === undefined) { - return; - } - expect(body.unsubscribe_rooms).toEqual([unsubRoomId]); - expect(body.room_subscriptions).not.toHaveProperty(unsubRoomId); - expect(body.room_subscriptions).toHaveProperty(subRoomId); + return body.txn_id && body.room_subscriptions?.[subRoomId]; + }; + const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => { + if (!request.url().includes("/sync")) return false; + const body = request.postDataJSON(); + return ( + body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId] + ); }; - let promise = page.waitForRequest(/sync/); - - // Select the Test Room - await page.getByRole("treeitem", { name: "Apple", exact: true }).click(); - - // and wait for playwright to get the request - const roomSubscriptions = (await promise).postDataJSON().room_subscriptions; + // Select the Test Room and wait for playwright to get the request + const [request] = await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomAId)), + page.getByRole("treeitem", { name: "Apple", exact: true }).click(), + ]); + const roomSubscriptions = request.postDataJSON().room_subscriptions; expect(roomSubscriptions, "room_subscriptions is object").toBeDefined(); - // Switch to another room - promise = page.waitForRequest(/sync/); - await page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(); - assertUnsubExists(await promise, roomPId, roomAId); + // Switch to another room and wait for playwright to get the request + await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomPId)), + page.waitForRequest(matchRoomUnsubRequest(roomAId)), + page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), + ]); - // And switch to even another room - promise = page.waitForRequest(/sync/); - await page.getByRole("treeitem", { name: "Apple", exact: true }).click(); - assertUnsubExists(await promise, roomPId, roomAId); + // And switch to even another room and wait for playwright to get the request + await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomOId)), + page.waitForRequest(matchRoomUnsubRequest(roomPId)), + page.getByRole("treeitem", { name: "Orange", exact: true }).click(), + ]); // TODO: Add tests for encrypted rooms }); diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index c4c69440b5..d3d3cb352b 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -334,17 +334,17 @@ 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 user - the user to mention in the first message * @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, + user: Credentials, hasMention = true, ) { if (hasMention) { diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 6135e47278..683577dce4 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -15,6 +15,7 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { isDendrite, "due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283", ); + test.use({ displayName: "Alice", botCreateOpts: { displayName: "Other User" }, @@ -79,7 +80,7 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { { tag: "@screenshot" }, async ({ room1, room2, util, msg, user }) => { await util.goTo(room2); - await util.populateThreads(user, room1, room2, msg); + await util.populateThreads(room1, room2, msg, user); // The indicator should be shown await util.assertHighlightIndicator(); @@ -97,7 +98,7 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, user }) => { await util.goTo(room2); - await util.populateThreads(user, room1, room2, msg); + await util.populateThreads(room1, room2, msg, user); // Click on the first room in TAC await util.openTac(); @@ -120,7 +121,7 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { test("should order by recency after notification level", async ({ room1, room2, util, msg, user }) => { await util.goTo(room2); - await util.populateThreads(user, room1, room2, msg, false); + await util.populateThreads(room1, room2, msg, user, false); await util.openTac(); await util.assertRoomsInTac([ diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index da0ad64eee..e60c93c27d 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -6,7 +6,15 @@ 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 as baseExpect, Locator, Page, ExpectMatcherState, ElementHandle } from "@playwright/test"; +import { + expect as baseExpect, + Locator, + Page, + ExpectMatcherState, + ElementHandle, + PlaywrightTestArgs, + Fixtures as _Fixtures, +} from "@playwright/test"; import { sanitizeForFilePath } from "playwright-core/lib/utils"; import AxeBuilder from "@axe-core/playwright"; import _ from "lodash"; @@ -19,7 +27,7 @@ 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"; +import { Options, Services, test as base } from "./services.ts"; // Enable experimental service worker support // See https://playwright.dev/docs/service-workers-experimental#how-to-enable @@ -45,7 +53,7 @@ interface CredentialsWithDisplayName extends Credentials { displayName: string; } -export interface Fixtures { +export interface TestFixtures { axe: AxeBuilder; checkA11y: () => Promise; @@ -101,7 +109,10 @@ export interface Fixtures { webserver: Webserver; } -export const test = base.extend({ +type CombinedTestFixtures = PlaywrightTestArgs & TestFixtures; +export type Fixtures = _Fixtures; + +export const test = base.extend({ context: async ({ context }, use, testInfo) => { // We skip tests instead of using grep-invert to still surface the counts in the html report test.skip( @@ -137,12 +148,12 @@ export const test = base.extend({ }, displayName: undefined, - credentials: async ({ homeserver, displayName: testDisplayName }, use) => { + credentials: async ({ context, homeserver, displayName: testDisplayName }, use, testInfo) => { const names = ["Alice", "Bob", "Charlie", "Daniel", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Judy"]; const password = _.uniqueId("password_"); const displayName = testDisplayName ?? _.sample(names)!; - const credentials = await homeserver.registerUser("user", password, displayName); + const credentials = await homeserver.registerUser(`user_${testInfo.testId}`, password, displayName); console.log(`Registered test user ${credentials.userId} with displayname ${displayName}`); await use({ @@ -167,6 +178,7 @@ export const test = base.extend({ window.localStorage.setItem( "mx_local_settings", JSON.stringify({ + // Retain any other settings which may have already been set ...JSON.parse(window.localStorage.getItem("mx_local_settings") || "{}"), // Ensure the language is set to a consistent value language: "en", diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index ba8a5bdb28..63847ecbfa 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -41,6 +41,7 @@ export interface Credentials { homeServer: string; password: string | null; // null for password-less users displayName?: string; + username: string; // the localpart of the userId } export type HomeserverType = "synapse" | "dendrite" | "pinecone"; diff --git a/playwright/plugins/homeserver/synapse/consentHomeserver.ts b/playwright/plugins/homeserver/synapse/consentHomeserver.ts index 348b6da4fe..e714e8a9c1 100644 --- a/playwright/plugins/homeserver/synapse/consentHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/consentHomeserver.ts @@ -6,53 +6,57 @@ 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 { 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"); - - 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", +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", }, - 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); + 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); + }, + { scope: "worker" }, + ], + + context: async ({ homeserverType, context }, use, testInfo) => { + testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); + await use(context); }, }; diff --git a/playwright/plugins/homeserver/synapse/emailHomeserver.ts b/playwright/plugins/homeserver/synapse/emailHomeserver.ts index c9a5daea7a..f7dee7b01a 100644 --- a/playwright/plugins/homeserver/synapse/emailHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/emailHomeserver.ts @@ -6,24 +6,29 @@ 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 { Options, 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: 1025, + notif_from: "Your Friendly %(app)s homeserver ", + app_name: "my_branded_matrix_server", + }, + }); + await use(container); + }, + { scope: "worker" }, + ], -export const emailHomeserver: Fixtures = { - _homeserver: async ({ homeserverType, _homeserver: container, mailhog }, use, testInfo) => { + context: async ({ homeserverType, context }, use, testInfo) => { testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); - container.withConfig({ - enable_registration_without_verification: undefined, - disable_msisdn_registration: undefined, - registrations_require_3pid: ["email"], - email: { - smtp_host: "mailhog", - smtp_port: 1025, - notif_from: "Your Friendly %(app)s homeserver ", - app_name: "my_branded_matrix_server", - }, - }); - await use(container); + await use(context); }, }; diff --git a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts index 4dc165bb9b..239cd00eeb 100644 --- a/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/legacyOAuthHomeserver.ts @@ -6,44 +6,50 @@ 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 { TestContainers } from "testcontainers"; -import { Options, Services } from "../../../services.ts"; import { OAuthServer } from "../../oauth_server"; +import { Fixtures } from "../../../element-web-test.ts"; -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(); +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 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(); + ], + }); + await use(container); + server.stop(); + }, + { scope: "worker" }, + ], + + context: async ({ homeserverType, context }, use, testInfo) => { + testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); + await use(context); }, }; diff --git a/playwright/plugins/homeserver/synapse/masHomeserver.ts b/playwright/plugins/homeserver/synapse/masHomeserver.ts index 4a4c331571..63260c4b89 100644 --- a/playwright/plugins/homeserver/synapse/masHomeserver.ts +++ b/playwright/plugins/homeserver/synapse/masHomeserver.ts @@ -6,60 +6,57 @@ 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, Services } from "../../../services.ts"; -import { Fixtures as BaseFixtures } from "../../../element-web-test.ts"; +import { Fixtures } from "../../../element-web-test.ts"; import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts"; -type Fixture = PlaywrightTestArgs & Services & BaseFixtures & Options; -export const masHomeserver: Fixtures = { - mas: async ({ homeserverType, _homeserver: homeserver, logger, network, postgres, mailhog }, use, testInfo) => { - testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); - - const config = { - clients: [ - { - client_id: "0000000000000000000SYNAPSE", - client_auth_method: "client_secret_basic", - client_secret: "SomeRandomSecret", +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://homeserver:8008", }, - ], - matrix: { - homeserver: "localhost", - secret: "AnotherRandomSecret", - endpoint: "http://homeserver:8008", - }, - }; + }; - const container = await new MatrixAuthenticationServiceContainer(postgres) - .withNetwork(network) - .withNetworkAliases("mas") - .withLogConsumer(logger.getConsumer("mas")) - .withConfig(config) - .start(); + 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/`, - 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, + 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/`, + 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, + }, }, - }, - }); + }); - await use(container); - await container.stop(); - }, + await use(container); + await container.stop(); + }, + { scope: "worker" }, + ], config: async ({ homeserver, context, mas }, use) => { const issuer = `${mas.baseUrl}/`; @@ -82,4 +79,9 @@ export const masHomeserver: Fixtures = { default_server_config: wellKnown, }); }, + + context: async ({ homeserverType, context }, use, testInfo) => { + testInfo.skip(homeserverType !== "synapse", "does not yet support MAS"); + await use(context); + }, }; diff --git a/playwright/services.ts b/playwright/services.ts index 3ec17624d0..508671c4fa 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -36,105 +36,132 @@ export interface Options { homeserverType: HomeserverType; } -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 ({ logger, network }, use) => { - const container = await new PostgreSqlContainer() - .withNetwork(network) - .withNetworkAliases("postgres") - .withLogConsumer(logger.getConsumer("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(); - }, +export const test = base.extend<{}, Services & Options>({ + logger: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use, testInfo) => { + const logger = new ContainerLogger(); + await use(logger); + }, + { scope: "worker" }, + ], + network: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + const network = await new Network().start(); + await use(network); + await network.stop(); + }, + { scope: "worker" }, + ], + postgres: [ + async ({ logger, network }, use) => { + const container = await new PostgreSqlContainer() + .withNetwork(network) + .withNetworkAliases("postgres") + .withLogConsumer(logger.getConsumer("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(); + }, + { scope: "worker" }, + ], - 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); - await container.stop(); - }, - mailhogClient: async ({ mailhog: container }, use) => { - await use(mailhog({ host: container.getHost(), port: container.getMappedPort(8025) })); - }, + 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); + await container.stop(); + }, + { scope: "worker" }, + ], + mailhogClient: [ + async ({ mailhog: container }, use) => { + const client = mailhog({ host: container.getHost(), port: container.getMappedPort(8025) }); + await use(client); + }, + { scope: "worker" }, + ], - synapseConfigOptions: [{}, { option: true }], - 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; - } + synapseConfigOptions: [{}, { option: true, scope: "worker" }], + homeserverType: ["synapse", { option: true, scope: "worker" }], + _homeserver: [ + async ({ homeserverType }, use) => { + let container: HomeserverContainer; + switch (homeserverType) { + case "synapse": + container = new SynapseContainer(); + break; + case "dendrite": + container = new DendriteContainer(); + break; + case "pinecone": + container = new PineconeContainer(); + break; + } - await use(container); - }, - homeserver: async ( - { homeserverType, logger, network, _homeserver: homeserver, synapseConfigOptions, mas }, - use, - testInfo, - ) => { + await use(container); + }, + { scope: "worker" }, + ], + homeserver: [ + async ({ homeserverType, logger, network, _homeserver: homeserver, synapseConfigOptions, mas }, use) => { + if (homeserver instanceof SynapseContainer) { + homeserver.withConfig(synapseConfigOptions); + } + + const container = await homeserver + .withNetwork(network) + .withNetworkAliases("homeserver") + .withLogConsumer(logger.getConsumer(homeserverType)) + .start(); + + await use(container); + await container.stop(); + }, + { scope: "worker" }, + ], + mas: [ + // eslint-disable-next-line no-empty-pattern + 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); + }, + { scope: "worker" }, + ], + + 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}`, ); - if (homeserver instanceof SynapseContainer) { - homeserver.withConfig(synapseConfigOptions); - } - - const container = await homeserver - .withNetwork(network) - .withNetworkAliases("homeserver") - .withLogConsumer(logger.getConsumer(homeserverType)) - .start(); - - 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); + homeserver.setRequest(request); + await logger.testStarted(testInfo); + await use(context); + await logger.testFinished(testInfo); }, }); diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png index 465e58ca39..7a108b441c 100644 Binary files a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png index cc64e8bf78..c66606efcd 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png index a634e876bc..02cbff0e3f 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index d01aae804c..fa3cf90430 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index 6e8e2fbddf..910040e20f 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png index c641f2a0aa..655d45bc4a 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png index b9d81c5d5a..e2bd16fb5a 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png differ diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png index 2fb39b9f5f..28483d5815 100644 Binary files a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png and b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png differ diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png index bfaa18d4c3..e2c6718a93 100644 Binary files a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png and b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png differ diff --git a/playwright/testcontainers/HomeserverContainer.ts b/playwright/testcontainers/HomeserverContainer.ts index bbe075f2c9..09eea7da77 100644 --- a/playwright/testcontainers/HomeserverContainer.ts +++ b/playwright/testcontainers/HomeserverContainer.ts @@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { AbstractStartedContainer, GenericContainer } from "testcontainers"; +import { APIRequestContext } from "@playwright/test"; import { StartedSynapseContainer } from "./synapse.ts"; import { HomeserverInstance } from "../plugins/homeserver"; @@ -16,4 +17,6 @@ export interface HomeserverContainer extends GenericContainer { start(): Promise; } -export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {} +export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance { + setRequest(request: APIRequestContext): void; +} diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index 484b828b52..629ea70c65 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -6,7 +6,6 @@ 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 { set } from "lodash"; @@ -208,11 +207,7 @@ const DEFAULT_CONFIG = { export class DendriteContainer extends GenericContainer implements HomeserverContainer { private config: typeof DEFAULT_CONFIG; - constructor( - private request: APIRequestContext, - image = "matrixdotorg/dendrite-monolith:main", - binary = "/usr/bin/dendrite", - ) { + constructor(image = "matrixdotorg/dendrite-monolith:main", binary = "/usr/bin/dendrite") { super(image); this.config = deepCopy(DEFAULT_CONFIG); @@ -254,13 +249,12 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon container, `http://${container.getHost()}:${container.getMappedPort(8008)}`, 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"); + constructor() { + super("matrixdotorg/dendrite-demo-pinecone:main", "/usr/bin/dendrite-demo-pinecone"); } } diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index b547da5dfd..65b1222719 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -141,7 +141,7 @@ export type SynapseConfigOptions = Partial; export class SynapseContainer extends GenericContainer implements HomeserverContainer { private config: typeof DEFAULT_CONFIG; - constructor(private readonly request: APIRequestContext) { + constructor() { super(`ghcr.io/element-hq/synapse:${TAG}`); this.config = deepCopy(DEFAULT_CONFIG); @@ -221,23 +221,26 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont await super.start(), `http://localhost:${port}`, this.config.registration_shared_secret, - this.request, ); } } export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { private adminToken?: string; + private request?: APIRequestContext; constructor( container: StartedTestContainer, public readonly baseUrl: string, private readonly registrationSharedSecret: string, - private readonly request: APIRequestContext, ) { super(container); } + public setRequest(request: APIRequestContext): void { + this.request = request; + } + private async registerUserInternal( username: string, password: string, @@ -273,6 +276,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements deviceId: data.device_id, password, displayName, + username, }; } @@ -300,6 +304,7 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements userId: json.user_id, deviceId: json.device_id, homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"), + username: userId.slice(1).split(":")[0], }; } diff --git a/playwright/testcontainers/utils.ts b/playwright/testcontainers/utils.ts index 36b8d8aabf..1339e9c2fc 100644 --- a/playwright/testcontainers/utils.ts +++ b/playwright/testcontainers/utils.ts @@ -24,6 +24,12 @@ export class ContainerLogger { }; } + public async testStarted(testInfo: TestInfo) { + for (const container in this.logs) { + this.logs[container] = ""; + } + } + public async testFinished(testInfo: TestInfo) { if (testInfo.status !== "passed") { for (const container in this.logs) { diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index efe3a4e75a..642ae1e567 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1276,7 +1276,7 @@ "error_already_invited_space": "Die Person wurde bereits eingeladen", "error_already_joined_room": "Die Person ist bereits im Raum", "error_already_joined_space": "Die Person ist bereits im Space", - "error_bad_state": "Verbannte Nutzer können nicht eingeladen werden.", + "error_bad_state": "Gesperrte Benutzer können nicht eingeladen werden.", "error_dm": "Wir konnten deine Direktnachricht nicht erstellen.", "error_find_room": "Beim Einladen der Nutzer lief etwas schief.", "error_find_user_description": "Folgende Nutzer konnten nicht eingeladen werden, da sie nicht existieren oder ungültig sind: %(csvNames)s", @@ -1317,7 +1317,7 @@ "unable_find_profiles_invite_label_default": "Dennoch einladen", "unable_find_profiles_invite_never_warn_label_default": "Trotzdem einladen und mich nicht mehr warnen", "unable_find_profiles_title": "Eventuell existieren folgende Benutzer nicht", - "unban_first_title": "Benutzer kann nicht eingeladen werden, solange er nicht entbannt ist" + "unban_first_title": "Benutzer kann nicht eingeladen werden, solange die Sperre nicht aufgehoben worden ist." }, "inviting_user1_and_user2": "Lade %(user1)s und %(user2)s ein", "inviting_user_and_n_others": { @@ -3732,8 +3732,8 @@ "ban_button_room": "Bannen", "ban_button_space": "Bannen", "ban_room_confirm_title": "Aus %(roomName)s verbannen", - "ban_space_everything": "Überall wo ich die Rechte dazu habe bannen", - "ban_space_specific": "In ausgewählten Räumen und Spaces bannen", + "ban_space_everything": "Überall sperren, wo ich die Rechte dazu habe", + "ban_space_specific": "Sperre sie in ausgewählten Chatrooms und Spaces", "count_of_sessions": { "other": "%(count)s Sitzungen", "one": "%(count)s Sitzung" @@ -3798,11 +3798,11 @@ "room_unencrypted_detail": "Nachrichten in verschlüsselten Räumen können nur von dir und vom Empfänger gelesen werden.", "send_message": "Nachricht senden", "share_button": "Profil teilen", - "unban_button_room": "Entbannen", - "unban_button_space": "Entbannen", - "unban_room_confirm_title": "Von %(roomName)s entbannen", - "unban_space_everything": "Überall wo ich die Rechte dazu habe, entbannen", - "unban_space_specific": "In ausgewählten Räumen und Spaces entbannen", + "unban_button_room": "Sperrung für den Chatroom aufheben", + "unban_button_space": "Sperrung aus Space aufheben", + "unban_room_confirm_title": "Sperrung für %(roomName)s aufheben", + "unban_space_everything": "Die Verbannung überall aufheben wo ich die Rechte dazu habe", + "unban_space_specific": "In ausgewählten Chatrooms und Spaces die Sperrung für sie aufheben", "unban_space_warning": "Die Person wird keinen Zutritt zu Bereichen haben, in denen du nicht administrierst.", "unignore_button": "Nicht mehr ignorieren", "verify_button": "Nutzer verifizieren",