Playwright test for device rehydration (#34091)

* 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.
This commit is contained in:
Richard van der Hoff
2026-07-02 09:51:22 +00:00
committed by GitHub
parent 20dad858c5
commit f354453cab
4 changed files with 83 additions and 82 deletions
@@ -8,7 +8,15 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { createBot, logIntoElement } from "./utils.ts";
import {
autoJoin,
createBot,
createSharedEncryptedRoomWithUser,
enableKeyBackup,
logIntoElement,
logIntoElementAndVerify,
logOutOfElement,
} from "./utils.ts";
import { type Client } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
@@ -40,15 +48,10 @@ test.describe("Dehydration", () => {
await settings.getByRole("button", { name: "Verify this device" }).click();
await page.getByRole("button", { name: "Can't confirm?" }).click();
await page.getByRole("button", { name: "Continue" }).click();
await app.closeDialog();
// Set up recovery
await page.getByRole("button", { name: "Get recovery key" }).click();
await page.getByRole("button", { name: "Continue" }).click();
const recoveryKey = await page.getByTestId("recoveryKey").innerText();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("textbox").fill(recoveryKey);
await page.getByRole("button", { name: "Finish set up" }).click();
await page.getByRole("button", { name: "Close" }).click();
await enableKeyBackup(app);
await expectDehydratedDeviceEnabled(app);
@@ -61,28 +64,7 @@ test.describe("Dehydration", () => {
test("'Get recovery key' creates dehydrated device", async ({ app, credentials, page }) => {
await logIntoElement(page, credentials);
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
await settingsDialogLocator.getByRole("button", { name: "Get recovery key" }).click();
// First it displays an informative panel about the recovery key
await expect(settingsDialogLocator.getByRole("heading", { name: "Get recovery key" })).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
// Next, it displays the new recovery key. We click on the copy button.
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
const recoveryKey = await app.getClipboard();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
await expect(
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
).toBeVisible();
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
await app.settings.closeDialog();
await enableKeyBackup(app);
await expectDehydratedDeviceEnabled(app);
});
@@ -115,13 +97,7 @@ test.describe("Dehydration", () => {
await page.getByRole("button", { name: "Continue" }).click();
// And set up recovery
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Get recovery key" }).click();
await settings.getByRole("button", { name: "Continue" }).click();
const recoveryKey = await settings.getByTestId("recoveryKey").innerText();
await settings.getByRole("button", { name: "Continue" }).click();
await settings.getByRole("textbox").fill(recoveryKey);
await settings.getByRole("button", { name: "Finish set up" }).click();
await enableKeyBackup(app);
// There should be a brand new dehydrated device
await expectDehydratedDeviceEnabled(app);
@@ -132,34 +108,54 @@ test.describe("Dehydration", () => {
// Create a dehydrated device by setting up recovery (see "'Set up
// recovery' creates dehydrated device" test above)
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
await settingsDialogLocator.getByRole("button", { name: "Get recovery key" }).click();
// First it displays an informative panel about the recovery key
await expect(settingsDialogLocator.getByRole("heading", { name: "Get recovery key" })).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
// Next, it displays the new recovery key. We click on the copy button.
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
const recoveryKey = await app.getClipboard();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
await expect(
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
).toBeVisible();
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
await enableKeyBackup(app);
await expectDehydratedDeviceEnabled(app);
// After recovery is set up, we reset our cryptographic identity, which
// should drop the dehydrated device.
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
await settingsDialogLocator.getByRole("button", { name: "Reset cryptographic identity" }).click();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
await expectDehydratedDeviceDisabled(app);
});
test("Can read messages sent while logged out", async ({ page, user: credentials, app, bot: bob }) => {
const recoveryKey =
await test.step("Alice sets up cross-signing and recovery => a dehydrated device is created", async () => {
// Create an identity, then set up recovery, to create a dehydrated device.
await app.client.bootstrapCrossSigning(credentials);
const recoveryKey = await enableKeyBackup(app);
await expectDehydratedDeviceEnabled(app);
return recoveryKey;
});
const testRoomId = await test.step("Bob and Alice make a shared room", async () => {
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 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 back in, and should be able to view Bob's message", async () => {
await logIntoElementAndVerify(page, credentials, recoveryKey);
await app.viewRoomById(testRoomId);
await expect(page.getByText("test encrypted 1")).toBeVisible();
});
});
});
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
@@ -12,7 +12,7 @@ import { expect, test } from "../../element-web-test";
import {
autoJoin,
createSecondBotDevice,
createSharedRoomWithUser,
createSharedEncryptedRoomWithUser,
enableKeyBackup,
logIntoElementAndVerify,
logOutOfElement,
@@ -39,18 +39,7 @@ test.describe("Cryptography", function () {
await autoJoin(bob);
// create an encrypted room, and wait for Bob to join it.
testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, {
name: "TestRoom",
initial_state: [
{
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
},
],
});
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);
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { expect, test } from "../../element-web-test";
import { autoJoin, createSecondBotDevice, createSharedRoomWithUser, verify } from "./utils";
import { autoJoin, createSecondBotDevice, createSharedEncryptedRoomWithUser, verify } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
@@ -29,18 +29,7 @@ test.describe("Invisible cryptography", () => {
await autoJoin(bob);
// create an encrypted room
const testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, {
name: "TestRoom",
initial_state: [
{
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
},
],
});
const testRoomId = await createSharedEncryptedRoomWithUser(app, bob.credentials.userId);
// Verify Bob
await verify(app, bob);
+27
View File
@@ -406,6 +406,7 @@ export async function copyAndContinue(page: Page) {
* @param opts - other options for the createRoom call
*
* @returns a promise which resolves to the room ID
* @see createSharedEncryptedRoomWithUser
*/
export async function createSharedRoomWithUser(
app: ElementAppPage,
@@ -423,6 +424,32 @@ export async function createSharedRoomWithUser(
return roomId;
}
/**
* Create a shared, encrypted room with the given user, and wait for them to join
*
* @param other - UserID of the other user
* @param opts - other options for the createRoom call
*
* @returns a promise which resolves to the room ID
* @see createSharedRoomWithUser
*/
export async function createSharedEncryptedRoomWithUser(
app: ElementAppPage,
other: string,
opts: Omit<ICreateRoomOpts, "invite"> = { name: "TestRoom" },
): Promise<string> {
opts = structuredClone(opts);
opts.initial_state ??= [];
opts.initial_state.push({
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
});
return createSharedRoomWithUser(app, other, opts);
}
/**
* Send a message in the current room
* @param page