feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "./index";
|
||||
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
|
||||
import { bootstrapCrossSigningForClient } from "../../../pages/client";
|
||||
|
||||
test.describe("Advanced section in Encryption tab", () => {
|
||||
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
|
||||
const clientHandle = await app.client.prepareClient();
|
||||
// Reset cross signing in order to have a verified session
|
||||
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
|
||||
});
|
||||
|
||||
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util, axe }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
|
||||
await expect(section.getByText(deviceId)).toBeVisible();
|
||||
|
||||
await expect(section).toMatchScreenshot("encryption-details.png", {
|
||||
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should show the import room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Import keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show the export room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Export keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should reset the cryptographic identity",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, util }) => {
|
||||
const tab = await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
|
||||
await tab.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Fill password dialog and validate
|
||||
const dialog = page.locator(".mx_InteractiveAuthDialog");
|
||||
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();
|
||||
|
||||
// After resetting the identity, the user should set up a new recovery key
|
||||
await expect(
|
||||
util.getEncryptionRecoverySection().getByRole("button", { name: "Get recovery key" }),
|
||||
).toBeVisible();
|
||||
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from ".";
|
||||
import {
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
deleteCachedSecrets,
|
||||
verifySession,
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Encryption tab", () => {
|
||||
test.use({ displayName: "Alice" });
|
||||
|
||||
test.describe("when encryption is set up", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const botCredentials = { ...credentials };
|
||||
delete botCredentials.accessToken; // use a new login for the bot
|
||||
const res = await createBot(page, homeserver, botCredentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
test(
|
||||
"should show a 'Verify this device' button if the device is unverified",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const content = util.getEncryptionTabContent();
|
||||
|
||||
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
||||
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
||||
await expect(verifyButton).toBeVisible();
|
||||
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
|
||||
await verifyButton.click();
|
||||
|
||||
await util.verifyDevice(recoveryKey);
|
||||
|
||||
// Prevent flakiness by scrolling to top of the tab
|
||||
await page.getByRole("heading", { name: "Key storage" }).scrollIntoViewIfNeeded();
|
||||
|
||||
await expect(content).toMatchScreenshot("default-tab.png", {
|
||||
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// 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 checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
||||
//
|
||||
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
// We simulate this case by deleting the cached secrets in the indexedDB.
|
||||
test(
|
||||
"should prompt to enter the recovery key when the secrets are not cached locally",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
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 dialog.getByRole("heading", { name: "Key storage" }).scrollIntoViewIfNeeded();
|
||||
await expect(dialog).toMatchScreenshot("default-tab.png", {
|
||||
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// 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 checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
|
||||
page,
|
||||
app,
|
||||
util,
|
||||
}) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
|
||||
await util.openEncryptionTab();
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||
|
||||
// The user is prompted to reset their identity
|
||||
await expect(
|
||||
dialog.getByText("Forgot your recovery key? You’ll need to reset your digital identity."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.openEncryptionTab();
|
||||
|
||||
await page.getByRole("switch", { name: "Allow key storage" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
|
||||
|
||||
const deleteRequestPromises = [
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
|
||||
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
|
||||
];
|
||||
|
||||
await page.getByRole("button", { name: "Delete key storage" }).click();
|
||||
|
||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
|
||||
|
||||
for (const prom of deleteRequestPromises) {
|
||||
const request = await prom;
|
||||
expect(request.method()).toBe("PUT");
|
||||
expect(request.postData()).toBe(JSON.stringify({}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("when encryption is not set up", () => {
|
||||
test("'Verify this device' allows us to become verified", async ({
|
||||
page,
|
||||
user,
|
||||
credentials,
|
||||
app,
|
||||
}, workerInfo) => {
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
|
||||
// Initially, our device is not verified
|
||||
await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible();
|
||||
|
||||
// We will reset our identity
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// First try cancelling and restarting
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Then click outside the dialog and restart
|
||||
await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true });
|
||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
||||
|
||||
// Finally we actually continue
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Now we are verified, so we see the Key storage toggle
|
||||
await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { type 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;
|
||||
}>({
|
||||
displayName: "Alice",
|
||||
|
||||
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: "Use recovery key" }).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) {
|
||||
// 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recovery section
|
||||
*/
|
||||
getEncryptionRecoverySection() {
|
||||
return this.page.getByTestId("recoveryPanel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encryption details section
|
||||
*/
|
||||
getEncryptionDetailsSection() {
|
||||
return this.page.getByTestId("encryptionDetails");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default key id of the secret storage to `null`
|
||||
*/
|
||||
async removeSecretStorageDefaultKeyId() {
|
||||
const client = await this.app.client.prepareClient();
|
||||
await client.evaluate(async (client) => {
|
||||
await client.secretStorage.setDefaultKeyId(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recovery key from the clipboard and fill in the input field
|
||||
* Then click on the finish button
|
||||
* @param title - The title of the dialog
|
||||
* @param confirmButtonLabel - The label of the confirm button
|
||||
* @param screenshot
|
||||
*/
|
||||
async confirmRecoveryKey(title: string, confirmButtonLabel: string, screenshot: `${string}.png`) {
|
||||
const dialog = this.getEncryptionTabContent();
|
||||
await expect(dialog.getByText(title, { exact: true })).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot(screenshot);
|
||||
|
||||
const clipboardContent = await this.app.getClipboard();
|
||||
await dialog.getByRole("textbox").fill(clipboardContent);
|
||||
const button = dialog.getByRole("button", { name: confirmButtonLabel });
|
||||
await button.click();
|
||||
// Button should disable immediately after clicking.
|
||||
await expect(button).toBeDisabled();
|
||||
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { createNewInstance } from "@element-hq/element-web-playwright-common";
|
||||
import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
import { type Page, type Browser, type TestInfo } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "./index";
|
||||
import { ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { createRoom, sendMessageInCurrentRoom, verifyApp } from "../../crypto/utils";
|
||||
import { type CredentialsOptionalAccessToken } from "../../../pages/bot";
|
||||
|
||||
test.describe("Other people's devices section in Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "alice",
|
||||
});
|
||||
|
||||
test("unverified devices should be able to decrypt while global blacklist is not toggled", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Create the room and invite bob
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice sends a message, which Bob should be able to decrypt
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
});
|
||||
|
||||
test("unverified devices should not be able to decrypt while global blacklist is toggled", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
util,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Enable blacklist toggle.
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const blacklistToggle = dialog.getByRole("switch", {
|
||||
name: "In encrypted rooms, only send messages to verified users",
|
||||
});
|
||||
await blacklistToggle.scrollIntoViewIfNeeded();
|
||||
await expect(blacklistToggle).toBeVisible();
|
||||
await blacklistToggle.click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Create the room and invite bob
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice sends a message, which Bob should not be able to decrypt
|
||||
await sendMessageInCurrentRoom(alicePage, "Undecryptable");
|
||||
await expect(
|
||||
bobPage.getByText(
|
||||
"The sender has blocked you from receiving this message because your device is unverified",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("verified devices should be able to decrypt while global blacklist is toggled", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
util,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Enable blacklist toggle.
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const blacklistToggle = dialog.getByRole("switch", {
|
||||
name: "In encrypted rooms, only send messages to verified users",
|
||||
});
|
||||
await blacklistToggle.scrollIntoViewIfNeeded();
|
||||
await expect(blacklistToggle).toBeVisible();
|
||||
await blacklistToggle.click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage, bobElementApp } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Create the room and invite bob
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
await bobElementApp.closeNotificationToast();
|
||||
|
||||
// Perform verification.
|
||||
await verifyApp("alice", aliceElementApp, "bob", bobElementApp);
|
||||
|
||||
// Alice sends a message, which Bob should be able to decrypt
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
});
|
||||
|
||||
test("setting per-room unverified blacklist toggle does not affect other rooms", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Alice creates the room and invites Bob.
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice configures her client to blacklist unverified users in this room.
|
||||
const dialog = await aliceElementApp.settings.openRoomSettings("Security & Privacy");
|
||||
await dialog.getByRole("switch", { name: "Only send messages to verified users." }).click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Alice sends a message which Bob should not be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Undecryptable");
|
||||
await expect(
|
||||
bobPage.getByText(
|
||||
"The sender has blocked you from receiving this message because your device is unverified",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
// Alice creates a second room and invites Bob.
|
||||
await createRoom(alicePage, "TestRoom2", true);
|
||||
await aliceElementApp.toggleRoomInfoPanel(); // should not be necessary, called in body of below
|
||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
||||
|
||||
// Bob accepts the invite.
|
||||
await bobPage.getByRole("option", { name: "TestRoom2" }).click();
|
||||
await bobPage.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
// Alice sends a message in the new room, which Bob should be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
});
|
||||
|
||||
test("setting per-room unverified blacklist toggle overrides global toggle", async ({
|
||||
page: alicePage,
|
||||
app: aliceElementApp,
|
||||
homeserver,
|
||||
browser,
|
||||
user: aliceCredentials,
|
||||
util,
|
||||
}, testInfo) => {
|
||||
await prepForEncryption(aliceElementApp, aliceCredentials);
|
||||
|
||||
// Enable blacklist toggle.
|
||||
let dialog = await util.openEncryptionTab();
|
||||
const blacklistToggle = dialog.getByRole("switch", {
|
||||
name: "In encrypted rooms, only send messages to verified users",
|
||||
});
|
||||
await blacklistToggle.scrollIntoViewIfNeeded();
|
||||
await expect(blacklistToggle).toBeVisible();
|
||||
await blacklistToggle.click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Create a second browser instance.
|
||||
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
|
||||
|
||||
// Alice creates the room and invites Bob.
|
||||
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
|
||||
|
||||
// Alice configures her client to allow sending to unverified users in this room.
|
||||
dialog = await aliceElementApp.settings.openRoomSettings("Security & Privacy");
|
||||
await dialog.getByRole("switch", { name: "Only send messages to verified users." }).click();
|
||||
await aliceElementApp.settings.closeDialog();
|
||||
|
||||
// Alice sends a message which Bob should be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Decryptable");
|
||||
await expect(bobPage.getByText("Decryptable")).toBeVisible();
|
||||
|
||||
// Alice creates a second room and invites Bob.
|
||||
await createRoom(alicePage, "TestRoom2", true);
|
||||
await aliceElementApp.toggleRoomInfoPanel(); // should not be necessary, called in body of below
|
||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
||||
|
||||
// Bob accepts the invite.
|
||||
await bobPage.getByRole("option", { name: "TestRoom2" }).click();
|
||||
await bobPage.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
// Alice sends a message in the new room, which Bob should not be able to decrypt.
|
||||
await sendMessageInCurrentRoom(alicePage, "Undecryptable");
|
||||
await expect(
|
||||
bobPage.getByText(
|
||||
"The sender has blocked you from receiving this message because your device is unverified",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
async function inviteBobToNewRoom(
|
||||
alicePage: Page,
|
||||
aliceElementApp: ElementAppPage,
|
||||
bobCredentials: CredentialsOptionalAccessToken,
|
||||
bobPage: Page,
|
||||
) {
|
||||
await createRoom(alicePage, "TestRoom", true);
|
||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
|
||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
||||
await bobPage.getByRole("button", { name: "Accept" }).click();
|
||||
}
|
||||
|
||||
async function newBrowser(
|
||||
homeserver: StartedHomeserverContainer,
|
||||
testInfo: TestInfo,
|
||||
browser: Browser,
|
||||
): Promise<{ bobCredentials: CredentialsOptionalAccessToken; bobPage: Page; bobElementApp: ElementAppPage }> {
|
||||
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
|
||||
const bobPage = await createNewInstance(browser, bobCredentials, {});
|
||||
const bobElementApp = new ElementAppPage(bobPage);
|
||||
await prepForEncryption(bobElementApp, bobCredentials);
|
||||
return { bobCredentials, bobPage, bobElementApp };
|
||||
}
|
||||
|
||||
async function prepForEncryption(app: ElementAppPage, credentials: CredentialsOptionalAccessToken): Promise<void> {
|
||||
await app.client.bootstrapCrossSigning(credentials);
|
||||
await app.closeKeyStorageToast();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from ".";
|
||||
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
|
||||
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
test.describe("Recovery section in Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const botCredentials = { ...credentials };
|
||||
delete botCredentials.accessToken; // use a new login for the bot
|
||||
const res = await createBot(page, homeserver, botCredentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
});
|
||||
|
||||
test(
|
||||
"should change the recovery key",
|
||||
{ tag: ["@screenshot", "@no-webkit"] },
|
||||
async ({ page, app, homeserver, credentials, util, context }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
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.getEncryptionRecoverySection()).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(
|
||||
"Enter your new recovery key",
|
||||
"Confirm new recovery key",
|
||||
"change-key-2-encryption-tab.png",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.removeSecretStorageDefaultKeyId();
|
||||
|
||||
// The key backup is deleted and the user needs to set it up
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const setupButton = dialog.getByRole("button", { name: "Get recovery key" });
|
||||
await expect(setupButton).toBeVisible();
|
||||
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("set-up-recovery.png");
|
||||
await setupButton.click();
|
||||
|
||||
// Display an informative panel about the recovery key
|
||||
await expect(dialog.getByRole("heading", { name: "Get recovery key" })).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(
|
||||
"Enter your recovery key to confirm",
|
||||
"Finish set up",
|
||||
"set-up-key-3-encryption-tab.png",
|
||||
);
|
||||
|
||||
// The recovery key is now set up and the user can change it
|
||||
await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||
|
||||
// Check that the current device is connected to key backup and the backup version is the expected one
|
||||
await checkDeviceIsConnectedKeyBackup(app, "1", true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user