Add e2e tests

florianduros/encryption-tab
Florian Duros 2024-12-18 18:52:13 +01:00
parent 895ad88d04
commit ba032a7eca
No known key found for this signature in database
GPG Key ID: A5BBB4041B493F15
13 changed files with 305 additions and 23 deletions

View File

@ -15,6 +15,7 @@ import {
awaitVerifier,
checkDeviceIsConnectedKeyBackup,
checkDeviceIsCrossSigned,
createBot,
doTwoWaySasVerification,
logIntoElement,
waitForVerificationRequest,
@ -28,29 +29,9 @@ test.describe("Device verification", () => {
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => {
// Visit the login page of the app, to load the matrix sdk
await page.goto("/#/login");
// wait for the page to load
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
// Create a new device for alice
aliceBotClient = new Bot(page, homeserver, {
bootstrapCrossSigning: true,
bootstrapSecretStorage: true,
});
aliceBotClient.setCredentials(credentials);
// Backup is prepared in the background. Poll until it is ready.
const botClientHandle = await aliceBotClient.prepareClient();
await expect
.poll(async () => {
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
cli.getCrypto()!.getActiveSessionBackupVersion(),
);
return expectedBackupVersion;
})
.not.toBe(null);
const res = await createBot(page, homeserver, credentials);
aliceBotClient = res.aliceBotClient;
expectedBackupVersion = res.expectedBackupVersion;
});
// Click the "Verify with another device" button, and have the bot client auto-accept it.

View File

@ -12,6 +12,7 @@ import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
import type {
CryptoEvent,
EmojiMapping,
GeneratedSecretStorageKey,
ShowSasCallbacks,
VerificationRequest,
Verifier,
@ -22,6 +23,47 @@ import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
/**
* Create a bot and wait for it to be ready to use.
* @param page - the page to use
* @param homeserver - the homeserver to use
* @param credentials - the credentials to use for the bot client
* @param ignoreSecretStorage - whether to ignore secret storage setup
*/
export async function createBot(
page: Page,
homeserver: HomeserverInstance,
credentials: Credentials,
): Promise<{ aliceBotClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
// Visit the login page of the app, to load the matrix sdk
await page.goto("/#/login");
// wait for the page to load
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
// Create a new device for alice
const aliceBotClient = new Bot(page, homeserver, {
bootstrapCrossSigning: true,
bootstrapSecretStorage: true,
});
aliceBotClient.setCredentials(credentials);
// Backup is prepared in the background. Poll until it is ready.
const botClientHandle = await aliceBotClient.prepareClient();
let expectedBackupVersion: string;
await expect
.poll(async () => {
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
cli.getCrypto()!.getActiveSessionBackupVersion(),
);
return expectedBackupVersion;
})
.not.toBe(null);
const recoveryKey = await aliceBotClient.getRecoveryKey();
return { aliceBotClient, recoveryKey, expectedBackupVersion };
}
/**
* wait for the given client to receive an incoming verification request, and automatically accept it
*

View File

@ -0,0 +1,118 @@
/*
* 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 { Page } from "@playwright/test";
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { ElementAppPage } from "../../../pages/ElementAppPage";
import { test as base, expect } from "../../../element-web-test";
export { expect };
/**
* Set up for the encryption tab test
*/
export const test = base.extend<{
util: Helpers;
}>({
util: async ({ page, app, bot }, use) => {
await use(new Helpers(page, app));
},
});
class Helpers {
constructor(
private page: Page,
private app: ElementAppPage,
) {}
/**
* Open the encryption tab
*/
openEncryptionTab() {
return this.app.settings.openUserSettings("Encryption");
}
/**
* Go through the device verification flow using the recovery key.
*/
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
// Select the security phrase
await this.page.getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
await this.enterRecoveryKey(recoveryKey);
await this.page.getByRole("button", { name: "Done" }).click();
}
/**
* Fill the recovery key in the dialog
* @param recoveryKey
*/
async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) {
// Select to use recovery key
await this.page.getByRole("button", { name: "use your Security Key" }).click();
// Fill the recovery key
const dialog = this.page.locator(".mx_Dialog");
await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey);
await dialog.getByRole("button", { name: "Continue" }).click();
}
/**
* Get the encryption tab content
*/
getEncryptionTabContent() {
return this.page.getByTestId("encryptionTab");
}
/**
* Delete the key backup for the given version
* @param backupVersion
*/
async deleteKeyBackup(backupVersion: string) {
const client = await this.app.client.prepareClient();
await client.evaluate(async (client, backupVersion) => {
await client.getCrypto()?.deleteKeyBackupVersion(backupVersion);
}, backupVersion);
}
/**
* Get the security key from the clipboard and fill in the input field
* Then click on the finish button
* @param screenshot
*/
async confirmRecoveryKey(screenshot: `${string}.png`) {
const dialog = this.getEncryptionTabContent();
await expect(dialog.getByText("Enter your recovery key to confirm")).toBeVisible();
await expect(dialog).toMatchScreenshot(screenshot);
const handle = await this.page.evaluateHandle(() => navigator.clipboard.readText());
const clipboardContent = await handle.jsonValue();
await dialog.getByRole("textbox").fill(clipboardContent);
await dialog.getByRole("button", { name: "Finish set up" }).click();
await expect(dialog).toMatchScreenshot("default-recovery.png");
}
/**
* Remove the cached secrets from the indexedDB
*/
async deleteCachedSecrets() {
await this.page.evaluate(async () => {
const removeCachedSecrets = new Promise((resolve) => {
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
request.onsuccess = async (event: Event & { target: { result: IDBDatabase } }) => {
const db = event.target.result;
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
request.onsuccess = () => {
db.close();
resolve(undefined);
};
};
});
await removeCachedSecrets;
});
await this.page.reload();
}
}

View File

@ -0,0 +1,141 @@
/*
* 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 { test, expect } from ".";
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from ".";
import {
checkDeviceIsConnectedKeyBackup,
checkDeviceIsCrossSigned,
createBot,
verifySession,
} from "../../crypto/utils";
test.describe("Recovery section in Encryption tab", () => {
test.use({
displayName: "Alice",
});
let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => {
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
expectedBackupVersion = res.expectedBackupVersion;
});
test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab();
// The user can only verify the device
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click();
await util.verifyDevice(recoveryKey);
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await app.closeDialog();
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
});
test(
"should change the recovery key",
{ tag: "@screenshot" },
async ({ page, app, homeserver, credentials, util, context }) => {
await verifySession(app, "new passphrase");
const dialog = await util.openEncryptionTab();
// The user can only change the recovery key
const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
await expect(changeButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
await changeButton.click();
// Display the new recovery key and click on the copy button
await expect(dialog.getByText("Change recovery key?")).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("change-key-1-encryption-tab.png", {
mask: [dialog.getByTestId("recoveryKey")],
});
await dialog.getByRole("button", { name: "Copy" }).click();
await dialog.getByRole("button", { name: "Continue" }).click();
// Confirm the recovery key
await util.confirmRecoveryKey("change-key-2-encryption-tab.png");
},
);
test("should setup the recovery key", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, "new passphrase");
await util.deleteKeyBackup(expectedBackupVersion);
// The key backup is deleted and the user needs to set up it
const dialog = await util.openEncryptionTab();
const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
await expect(setupButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png");
await setupButton.click();
// Display an informative panel about the recovery key
await expect(dialog.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-1-encryption-tab.png");
await dialog.getByRole("button", { name: "Continue" }).click();
// Display the new recovery key and click on the copy button
await expect(dialog.getByText("Save your recovery key somewhere safe")).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-2-encryption-tab.png", {
mask: [dialog.getByTestId("recoveryKey")],
});
await dialog.getByRole("button", { name: "Copy" }).click();
await dialog.getByRole("button", { name: "Continue" }).click();
// Confirm the recovery key
await util.confirmRecoveryKey("set-up-key-3-encryption-tab.png");
await app.closeDialog();
// Check that the current device is connected to key backup and the backup version is the expected one
await checkDeviceIsConnectedKeyBackup(page, "2", true);
});
test(
"should enter the recovery key when the secrets are not cached",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
await verifySession(app, "new passphrase");
// We need to delete the cached secrets
await util.deleteCachedSecrets();
await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-recovery.png");
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await app.closeDialog();
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
},
);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB