Add e2e tests
|
@ -15,6 +15,7 @@ import {
|
||||||
awaitVerifier,
|
awaitVerifier,
|
||||||
checkDeviceIsConnectedKeyBackup,
|
checkDeviceIsConnectedKeyBackup,
|
||||||
checkDeviceIsCrossSigned,
|
checkDeviceIsCrossSigned,
|
||||||
|
createBot,
|
||||||
doTwoWaySasVerification,
|
doTwoWaySasVerification,
|
||||||
logIntoElement,
|
logIntoElement,
|
||||||
waitForVerificationRequest,
|
waitForVerificationRequest,
|
||||||
|
@ -28,29 +29,9 @@ test.describe("Device verification", () => {
|
||||||
let expectedBackupVersion: string;
|
let expectedBackupVersion: string;
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||||
// Visit the login page of the app, to load the matrix sdk
|
const res = await createBot(page, homeserver, credentials);
|
||||||
await page.goto("/#/login");
|
aliceBotClient = res.aliceBotClient;
|
||||||
|
expectedBackupVersion = res.expectedBackupVersion;
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import type {
|
import type {
|
||||||
CryptoEvent,
|
CryptoEvent,
|
||||||
EmojiMapping,
|
EmojiMapping,
|
||||||
|
GeneratedSecretStorageKey,
|
||||||
ShowSasCallbacks,
|
ShowSasCallbacks,
|
||||||
VerificationRequest,
|
VerificationRequest,
|
||||||
Verifier,
|
Verifier,
|
||||||
|
@ -22,6 +23,47 @@ import { Client } from "../../pages/client";
|
||||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { Bot } from "../../pages/bot";
|
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
|
* wait for the given client to receive an incoming verification request, and automatically accept it
|
||||||
*
|
*
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 13 KiB |