From c060ec7051051a710cbfa9c4f3b7e7b74086428e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2025 15:18:06 +0000 Subject: [PATCH 1/3] Add a playwright test for backup reset / deleted A slightly tricky one to test but an important case that people can hit, and one that otherwise wouldn't get hit a lot during normal usage, so I think probably quite a useful test to have. Mostly though, I'm about to change this to a toast, so I'd like a test to assert that it still works. --- playwright/e2e/crypto/backups.spec.ts | 57 +++++++++++++++++++++++++++ playwright/e2e/csAPI.ts | 52 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 playwright/e2e/csAPI.ts diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index 40c7dc0ac6c..8d67c606cd4 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -11,6 +11,7 @@ import { type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { test as masTest, registerAccountMas } from "../oidc"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { TestClientServerAPI } from "../csAPI"; async function expectBackupVersionToBe(page: Page, version: string) { await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText( @@ -20,6 +21,9 @@ 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 a user an synthesizes an existing login, +// which is faster but leaves us without crypto set up. masTest.describe("Encryption state after registration", () => { masTest.skip(isDendrite, "does not yet support MAS"); @@ -46,6 +50,59 @@ masTest.describe("Encryption state after registration", () => { }); }); +masTest.describe("Key backup reset from elsewhere", () => { + masTest.skip(isDendrite, "does not yet support MAS"); + + masTest( + "Key backup is disabled when reset from elsewhere", + async ({ page, mailhog, request, masPrepare, 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, mailhog.api, 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.use({ displayName: "Hanako", diff --git a/playwright/e2e/csAPI.ts b/playwright/e2e/csAPI.ts new file mode 100644 index 00000000000..ce274cd55d6 --- /dev/null +++ b/playwright/e2e/csAPI.ts @@ -0,0 +1,52 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { APIRequestContext } from "playwright-core"; +import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; + +import { HomeserverInstance } from "../plugins/homeserver"; + +/** + * A small subset of the Client-Server API used to manipulate the state of the + * account on the homeserver independently of the client under test. + */ +export class TestClientServerAPI { + public constructor( + private request: APIRequestContext, + private homeserver: HomeserverInstance, + private accessToken: string, + ) {} + + public async getCurrentBackupInfo(): Promise { + const res = await this.request.get(`${this.homeserver.config.baseUrl}/_matrix/client/v3/room_keys/version`, { + headers: { Authorization: `Bearer ${this.accessToken}` }, + }); + + const body = await res.json(); + + return body; + } + + /** + * Calls the API directly to create a new backup version. + * @param algorithm The backup algorithm to use. + * @param authData The backup auth data + * @returns The version number of the new backup + */ + public async deleteBackupVersion(version: string): Promise { + const res = await this.request.delete( + `${this.homeserver.config.baseUrl}/_matrix/client/v3/room_keys/version/${version}`, + { + headers: { Authorization: `Bearer ${this.accessToken}` }, + }, + ); + + if (!res.ok) { + throw new Error(`Failed to delete backup version: ${res.status}`); + } + } +} From b7880e4eefd27fdfb47cbac66703c310e8125aa1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Jan 2025 15:33:05 +0000 Subject: [PATCH 2/3] Return directly Co-authored-by: Florian Duros --- playwright/e2e/csAPI.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/playwright/e2e/csAPI.ts b/playwright/e2e/csAPI.ts index ce274cd55d6..a3a744ac07f 100644 --- a/playwright/e2e/csAPI.ts +++ b/playwright/e2e/csAPI.ts @@ -26,9 +26,7 @@ export class TestClientServerAPI { headers: { Authorization: `Bearer ${this.accessToken}` }, }); - const body = await res.json(); - - return body; + return await res.json(); } /** From 829897ccc3b8f84639e8cd82e6f3e0a9d81230b8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Jan 2025 15:34:59 +0000 Subject: [PATCH 3/3] Fix tsdco --- playwright/e2e/csAPI.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/playwright/e2e/csAPI.ts b/playwright/e2e/csAPI.ts index a3a744ac07f..7fb7bece8d8 100644 --- a/playwright/e2e/csAPI.ts +++ b/playwright/e2e/csAPI.ts @@ -30,10 +30,8 @@ export class TestClientServerAPI { } /** - * Calls the API directly to create a new backup version. - * @param algorithm The backup algorithm to use. - * @param authData The backup auth data - * @returns The version number of the new backup + * Calls the API directly to delete the given backup version + * @param version The version to delete */ public async deleteBackupVersion(version: string): Promise { const res = await this.request.delete(