From 1b9aff2fb139de4a0a7df154b6d287274427166a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:07:57 +0100 Subject: [PATCH] Playwright test for dehydrated devices with MAS (#34095) * Device dehydration tests: use common util for recovery setup We have a utility method for setting up the recovery key, so let's use it. * Factor out shared method for creating encrypted rooms * Playwright test for device dehydration Add a playwright test to check that this stuff actually works. * Factor out `verifyAfterLogin` as a separate function * Playwright test for dehydrated devices with MAS --- .../e2e/crypto/dehydration-mas.spec.ts | 80 +++++++++++++++++++ .../playwright/e2e/crypto/dehydration.spec.ts | 5 +- .../e2e/crypto/event-shields.spec.ts | 6 +- apps/web/playwright/e2e/crypto/toasts.spec.ts | 10 ++- apps/web/playwright/e2e/crypto/utils.ts | 10 +-- 5 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 apps/web/playwright/e2e/crypto/dehydration-mas.spec.ts diff --git a/apps/web/playwright/e2e/crypto/dehydration-mas.spec.ts b/apps/web/playwright/e2e/crypto/dehydration-mas.spec.ts new file mode 100644 index 0000000000..e17d3e9240 --- /dev/null +++ b/apps/web/playwright/e2e/crypto/dehydration-mas.spec.ts @@ -0,0 +1,80 @@ +/* + Copyright 2026 Element Creations 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 { expect, test } from "../../element-web-test.ts"; +import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; +import { + autoJoin, + createSharedEncryptedRoomWithUser, + enableKeyBackup, + logOutOfElement, + verifyAfterLogin, +} from "./utils.ts"; +import { registerAccountMas } from "../oidc"; +import { Bot } from "../../pages/bot.ts"; + +test.use({ + ...masHomeserver, + synapseConfig: { + experimental_features: { + msc2697_enabled: false, + msc3814_enabled: true, + }, + }, +}); + +test.describe("Device dehydration, on a MAS-enabled homeserver", () => { + test("Can read messages sent while logged out", async ({ mailpitClient, homeserver, page, app }, testInfo) => { + test.slow(); + const aliceUserId = `alice_${testInfo.testId}`; + const alicePassword = "Pa$sW0rD!"; + + const recoveryKey = + await test.step("Alice registers and sets up recovery => a dehydrated device is created", async () => { + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + + await registerAccountMas(page, mailpitClient, aliceUserId, `${aliceUserId}@email.com`, alicePassword); + return await enableKeyBackup(app); + }); + + const [bob, testRoomId] = await test.step("Bob registers and joins a room with Alice", async () => { + const bob = new Bot(page, homeserver, { displayName: "Bob" }); + await autoJoin(bob); + + // Create an encrypted room, and wait for Bob to join it. + const testRoomId = await createSharedEncryptedRoomWithUser(app, bob.credentials.userId); + + // Even though Alice has seen Bob's join event, Bob may not have done so yet. Wait for the sync to arrive. + await bob.awaitRoomMembership(testRoomId); + return [bob, testRoomId]; + }); + + await test.step("Alice logs out", async () => { + await logOutOfElement(page); + }); + + await test.step("Bob sends a message", async () => { + await bob.sendMessage(testRoomId, "test encrypted 1"); + }); + + await test.step("Alice logs in again", async () => { + await page.getByRole("link", { name: "Sign in" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + + await expect(page.getByText("Continue to Element?")).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + + await verifyAfterLogin(page, recoveryKey); + await app.viewRoomById(testRoomId); + }); + + await test.step("Alice can decrypt Bob's message", async () => { + await expect(page.getByText("test encrypted 1")).toBeVisible(); + }); + }); +}); diff --git a/apps/web/playwright/e2e/crypto/dehydration.spec.ts b/apps/web/playwright/e2e/crypto/dehydration.spec.ts index ffe77f2892..6f79b41da3 100644 --- a/apps/web/playwright/e2e/crypto/dehydration.spec.ts +++ b/apps/web/playwright/e2e/crypto/dehydration.spec.ts @@ -14,8 +14,8 @@ import { createSharedEncryptedRoomWithUser, enableKeyBackup, logIntoElement, - logIntoElementAndVerify, logOutOfElement, + verifyAfterLogin, } from "./utils.ts"; import { type Client } from "../../pages/client.ts"; import { type ElementAppPage } from "../../pages/ElementAppPage.ts"; @@ -151,7 +151,8 @@ test.describe("Dehydration", () => { }); await test.step("Alice logs back in, and should be able to view Bob's message", async () => { - await logIntoElementAndVerify(page, credentials, recoveryKey); + await logIntoElement(page, credentials); + await verifyAfterLogin(page, recoveryKey); await app.viewRoomById(testRoomId); await expect(page.getByText("test encrypted 1")).toBeVisible(); }); diff --git a/apps/web/playwright/e2e/crypto/event-shields.spec.ts b/apps/web/playwright/e2e/crypto/event-shields.spec.ts index 94f91c9c4c..a610a98a64 100644 --- a/apps/web/playwright/e2e/crypto/event-shields.spec.ts +++ b/apps/web/playwright/e2e/crypto/event-shields.spec.ts @@ -14,9 +14,10 @@ import { createSecondBotDevice, createSharedEncryptedRoomWithUser, enableKeyBackup, - logIntoElementAndVerify, + logIntoElement, logOutOfElement, verify, + verifyAfterLogin, waitForDevices, } from "./utils"; import { bootstrapCrossSigningForClient } from "../../pages/client.ts"; @@ -170,7 +171,8 @@ test.describe("Cryptography", function () { window.localStorage.clear(); }); await page.reload(); - await logIntoElementAndVerify(page, aliceCredentials, securityKey); + await logIntoElement(page, aliceCredentials); + await verifyAfterLogin(page, securityKey); /* go back to the test room and find Bob's message again */ await app.viewRoomById(testRoomId); diff --git a/apps/web/playwright/e2e/crypto/toasts.spec.ts b/apps/web/playwright/e2e/crypto/toasts.spec.ts index 9267739305..c23ba8ea71 100644 --- a/apps/web/playwright/e2e/crypto/toasts.spec.ts +++ b/apps/web/playwright/e2e/crypto/toasts.spec.ts @@ -8,8 +8,8 @@ import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; import { assertNoToasts, getToast, rejectToast } from "@element-hq/element-web-playwright-common"; -import { test, expect } from "../../element-web-test"; -import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement, logIntoElementAndVerify } from "./utils"; +import { expect, test } from "../../element-web-test"; +import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement, verifyAfterLogin } from "./utils"; import { type Bot } from "../../pages/bot"; // Mask the background of the screenshot to avoid failing the test just because some @@ -29,7 +29,8 @@ test.describe("Key storage out of sync toast", () => { const res = await createBot(page, homeserver, credentials); recoveryKey = res.recoveryKey; - await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey); + await logIntoElement(page, credentials); + await verifyAfterLogin(page, recoveryKey.encodedPrivateKey); await deleteCachedSecrets(page); }); @@ -71,7 +72,8 @@ test.describe("'Turn on key storage' toast", () => { const recoveryKey = res.recoveryKey; botClient = res.botClient; - await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey); + await logIntoElement(page, credentials); + await verifyAfterLogin(page, recoveryKey.encodedPrivateKey); // We won't be prompted for crypto setup unless we have an e2e room, so make one await page diff --git a/apps/web/playwright/e2e/crypto/utils.ts b/apps/web/playwright/e2e/crypto/utils.ts index b558513db3..be86a1aa1e 100644 --- a/apps/web/playwright/e2e/crypto/utils.ts +++ b/apps/web/playwright/e2e/crypto/utils.ts @@ -216,14 +216,14 @@ export async function logIntoElement(page: Page, credentials: Credentials) { } /** - * Fill in the login form in Element with the given creds, and then complete the `CompleteSecurity` step, using the - * given recovery key. (Normally this will verify the new device using the secrets from 4S.) + * Complete the `CompleteSecurity` step that happens after login, using the given recovery key. + * (Normally this will verify the new device using the secrets from 4S.) * * Afterwards, waits for the application to redirect to the home page. + * + * This is normally useful after a call to {@link logIntoElement} or {@link logInAccountMas}. */ -export async function logIntoElementAndVerify(page: Page, credentials: Credentials, recoveryKey: string) { - await logIntoElement(page, credentials); - +export async function verifyAfterLogin(page: Page, recoveryKey: string) { await page.locator(".mx_AuthPage").getByRole("button", { name: "Use recovery key" }).click(); const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "Use recovery key" });