diff --git a/apps/web/playwright/e2e/crypto/dehydration.spec.ts b/apps/web/playwright/e2e/crypto/dehydration.spec.ts index ccb98e89df..ffe77f2892 100644 --- a/apps/web/playwright/e2e/crypto/dehydration.spec.ts +++ b/apps/web/playwright/e2e/crypto/dehydration.spec.ts @@ -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 { diff --git a/apps/web/playwright/e2e/crypto/event-shields.spec.ts b/apps/web/playwright/e2e/crypto/event-shields.spec.ts index e09fdaf9fc..94f91c9c4c 100644 --- a/apps/web/playwright/e2e/crypto/event-shields.spec.ts +++ b/apps/web/playwright/e2e/crypto/event-shields.spec.ts @@ -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); diff --git a/apps/web/playwright/e2e/crypto/invisible-crypto.spec.ts b/apps/web/playwright/e2e/crypto/invisible-crypto.spec.ts index db0961b560..d5aeba63cc 100644 --- a/apps/web/playwright/e2e/crypto/invisible-crypto.spec.ts +++ b/apps/web/playwright/e2e/crypto/invisible-crypto.spec.ts @@ -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); diff --git a/apps/web/playwright/e2e/crypto/utils.ts b/apps/web/playwright/e2e/crypto/utils.ts index 986d1151c8..b558513db3 100644 --- a/apps/web/playwright/e2e/crypto/utils.ts +++ b/apps/web/playwright/e2e/crypto/utils.ts @@ -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 = { name: "TestRoom" }, +): Promise { + 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