From b7f862361738c71403ee3b7eeb54e08d8955067f Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 3 Feb 2025 14:47:55 +0100 Subject: [PATCH] Encryption tab: hide `Advanced` section when the key storage is out of sync (#29129) * fix(encryption tab): hide the advanced section when the secrets are not cached locally The secret verification is now made at the level of `EncryptionUserSettingsTab` instead at the `RecoveryPanel` level. In the `EncryptionUserSettingsTab`, we decide to only display `RecoveryPanelOutOfSync` in case of uncached secrets. `RecoveryPanelOutOfSync` is simplified version of `RecoveryPanel` handling only the `secrets_not_cached` case. * refactor(encryption tab): simplify the `RecoveryPanel` without having to handle the missing secrets * test(encryption tab): move test about cached secrets in `EncryptionUserSettingsTab-test.tsx` * test(encryption tab): move e2e test which are testing all the encryption tab in `encryption-tab.spec.ts * refactor(encryption tab): move `RecoveryPanelOutOfSync` in its own file - fix typos - call onFinish after accessSecretStorage - onFinish doesn't need to be asynchronous * doc(encryption tab): improve documentation when the secrets are not cached locally * test(encryption tab): improve test documentation and naming * doc(encryption tab): improve `RecoveryPanelOutOfSync` documentation --- .../encryption-tab.spec.ts | 96 ++++++++++++++++++ .../encryption-user-tab/recovery.spec.ts | 75 +------------- .../default-tab-linux.png | Bin .../out-of-sync-recovery-linux.png | Bin 0 -> 22347 bytes .../verify-device-encryption-tab-linux.png | Bin .../out-of-sync-recovery-linux.png | Bin 21423 -> 0 bytes .../settings/encryption/RecoveryPanel.tsx | 79 +++----------- .../encryption/RecoveryPanelOutOfSync.tsx | 58 +++++++++++ .../tabs/user/EncryptionUserSettingsTab.tsx | 46 +++++++-- .../encryption/RecoveryPanel-test.tsx | 19 ---- .../__snapshots__/RecoveryPanel-test.tsx.snap | 61 ----------- .../user/EncryptionUserSettingsTab-test.tsx | 30 ++++++ .../EncryptionUserSettingsTab-test.tsx.snap | 70 +++++++++++++ 13 files changed, 307 insertions(+), 227 deletions(-) create mode 100644 playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts rename playwright/snapshots/settings/encryption-user-tab/{recovery.spec.ts => encryption-tab.spec.ts}/default-tab-linux.png (100%) create mode 100644 playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png rename playwright/snapshots/settings/encryption-user-tab/{recovery.spec.ts => encryption-tab.spec.ts}/verify-device-encryption-tab-linux.png (100%) delete mode 100644 playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/out-of-sync-recovery-linux.png create mode 100644 src/components/views/settings/encryption/RecoveryPanelOutOfSync.tsx diff --git a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts new file mode 100644 index 0000000000..79ee3fc7a5 --- /dev/null +++ b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts @@ -0,0 +1,96 @@ +/* + * 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 { 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", + }); + + 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 res = await createBot(page, homeserver, credentials); + 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); + + 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, "new passphrase"); + // 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 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); + }, + ); +}); diff --git a/playwright/e2e/settings/encryption-user-tab/recovery.spec.ts b/playwright/e2e/settings/encryption-user-tab/recovery.spec.ts index 316f305c97..8bb16f018b 100644 --- a/playwright/e2e/settings/encryption-user-tab/recovery.spec.ts +++ b/playwright/e2e/settings/encryption-user-tab/recovery.spec.ts @@ -5,53 +5,17 @@ * Please see LICENSE files in the repository root for full details. */ -import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; - import { test, expect } from "."; -import { - checkDeviceIsConnectedKeyBackup, - checkDeviceIsCrossSigned, - createBot, - deleteCachedSecrets, - verifySession, -} from "../../crypto/utils"; +import { checkDeviceIsConnectedKeyBackup, 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(); - 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); - - 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); + // The bot bootstraps cross-signing, creates a key backup and sets up a recovery key + await createBot(page, homeserver, credentials); }); test( @@ -121,37 +85,4 @@ test.describe("Recovery section in Encryption tab", () => { // Check that the current device is connected to key backup and the backup version is the expected one await checkDeviceIsConnectedKeyBackup(app, "1", 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 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 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(util.getEncryptionRecoverySection()).toMatchScreenshot("out-of-sync-recovery.png"); - await enterKeyButton.click(); - - // Fill the recovery key - await util.enterRecoveryKey(recoveryKey); - await expect(util.getEncryptionRecoverySection()).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 checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); - }, - ); }); diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png similarity index 100% rename from playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-tab-linux.png rename to playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png diff --git a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e6664a5f79b1e1a536f38cba312a836273a9f8ed GIT binary patch literal 22347 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

J&MN+Qgq6Fs@jEzU<@MUb=F9#+|64) zPfKQyH|+wS`1%d}9Quvq21{;W_VE^_98m)-mGjEVNQZFd7(Ea^N$A{Kb`S-71*-p% zY#nU-^|_O)0cz_}W2YH7g(+H$#%z6(FvhMa14a|?u1}C4=y3YkpJa|hbQ1y_DLFM( zq38sb9B_P#VIG6U>0G_<(S<71Q@i6sKV=O5D!!#WK=D904~{PQ?`qhxgW4m@riK@x z;A5^9plcU+&m^Nd1f$l42XXw!B_F$1t2epf8O-jF{0}c0pR}zVEqC$!^`)t0G-2{J z7-Ie?>gC1FDRpa`qrv9p$@FyFimKzwuN;99QEdzXZ{jNFYfWJtZGGFC`^L70<~G67 ggrt+;=|eQSIBl2Q(&o$JV7YWUT5!!`4ZBzW0qzmO^8f$< diff --git a/src/components/views/settings/encryption/RecoveryPanel.tsx b/src/components/views/settings/encryption/RecoveryPanel.tsx index cd89ba7617..129f698912 100644 --- a/src/components/views/settings/encryption/RecoveryPanel.tsx +++ b/src/components/views/settings/encryption/RecoveryPanel.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { JSX, useCallback, useEffect, useState } from "react"; +import React, { JSX } from "react"; import { Button, InlineSpinner } from "@vector-im/compound-web"; import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key"; @@ -13,18 +13,15 @@ import { SettingsSection } from "../shared/SettingsSection"; import { _t } from "../../../../languageHandler"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; import { SettingsHeader } from "../SettingsHeader"; -import { accessSecretStorage } from "../../../../SecurityManager"; -import { SettingsSubheader } from "../SettingsSubheader"; +import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; /** * The possible states of the recovery panel. * - `loading`: We are checking the recovery key and the secrets. * - `missing_recovery_key`: The user has no recovery key. - * - `secrets_not_cached`: The user has a recovery key but the secrets are not cached. - * This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. * - `good`: The user has a recovery key and the secrets are cached. */ -type State = "loading" | "missing_recovery_key" | "secrets_not_cached" | "good"; +type State = "loading" | "missing_recovery_key" | "good"; interface RecoveryPanelProps { /** @@ -40,29 +37,18 @@ interface RecoveryPanelProps { * This component allows the user to set up or change their recovery key. */ export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps): JSX.Element { - const [state, setState] = useState("loading"); - const isMissingRecoveryKey = state === "missing_recovery_key"; - const matrixClient = useMatrixClientContext(); - - const checkEncryption = useCallback(async () => { - const crypto = matrixClient.getCrypto()!; - - // Check if the user has a recovery key - const hasRecoveryKey = Boolean(await matrixClient.secretStorage.getDefaultKeyId()); - if (!hasRecoveryKey) return setState("missing_recovery_key"); - - // Check if the secrets are cached - const cachedSecrets = (await crypto.getCrossSigningStatus()).privateKeysCachedLocally; - const secretsOk = cachedSecrets.masterKey && cachedSecrets.selfSigningKey && cachedSecrets.userSigningKey; - if (!secretsOk) return setState("secrets_not_cached"); - - setState("good"); - }, [matrixClient]); - - useEffect(() => { - checkEncryption(); - }, [checkEncryption]); + const state = useAsyncMemo( + async () => { + // Check if the user has a recovery key + const hasRecoveryKey = Boolean(await matrixClient.secretStorage.getDefaultKeyId()); + if (hasRecoveryKey) return "good"; + else return "missing_recovery_key"; + }, + [matrixClient], + "loading", + ); + const isMissingRecoveryKey = state === "missing_recovery_key"; let content: JSX.Element; switch (state) { @@ -76,18 +62,6 @@ export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps): ); break; - case "secrets_not_cached": - content = ( - - ); - break; case "good": content = ( + + ); +} diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx index 4c5030cb58..0f4a164a07 100644 --- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @@ -20,6 +20,7 @@ import { SettingsSection } from "../../shared/SettingsSection"; import { SettingsSubheader } from "../../SettingsSubheader"; import { AdvancedPanel } from "../../encryption/AdvancedPanel"; import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel"; +import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync"; /** * The state in the encryption settings tab. @@ -32,12 +33,22 @@ import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel"; * - "set_recovery_key": The panel to show when the user is setting up their recovery key. * This happens when the user doesn't have a key a recovery key and the user clicks on "Set up recovery key" button of the RecoveryPanel. * - "reset_identity": The panel to show when the user is resetting their identity. + * - `secrets_not_cached`: The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. + * If the "set_up_encryption" and "secrets_not_cached" conditions are both filled, "set_up_encryption" prevails. + * */ -type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key" | "reset_identity"; +type State = + | "loading" + | "main" + | "set_up_encryption" + | "change_recovery_key" + | "set_recovery_key" + | "reset_identity" + | "secrets_not_cached"; export function EncryptionUserSettingsTab(): JSX.Element { const [state, setState] = useState("loading"); - const setUpEncryptionRequired = useSetUpEncryptionRequired(setState); + const checkEncryptionState = useCheckEncryptionState(setState); let content: JSX.Element; switch (state) { @@ -45,7 +56,10 @@ export function EncryptionUserSettingsTab(): JSX.Element { content = ; break; case "set_up_encryption": - content = ; + content = ; + break; + case "secrets_not_cached": + content = ; break; case "main": content = ( @@ -83,8 +97,12 @@ export function EncryptionUserSettingsTab(): JSX.Element { } /** - * Hook to check if the user needs to go through the SetupEncryption flow. + * Hook to check if the user needs: + * - to go through the SetupEncryption flow. + * - to enter their recovery key, if the secrets are not cached locally. + * * If the user needs to set up the encryption, the state will be set to "set_up_encryption". + * If the user secrets are not cached, the state will be set to "secrets_not_cached". * Otherwise, the state will be set to "main". * * The state is set once when the component is first mounted. @@ -93,23 +111,29 @@ export function EncryptionUserSettingsTab(): JSX.Element { * @param setState - callback passed from the EncryptionUserSettingsTab to set the current `State`. * @returns a callback function, which will re-run the logic and update the state. */ -function useSetUpEncryptionRequired(setState: (state: State) => void): () => Promise { +function useCheckEncryptionState(setState: (state: State) => void): () => Promise { const matrixClient = useMatrixClientContext(); - const setUpEncryptionRequired = useCallback(async () => { + const checkEncryptionState = useCallback(async () => { const crypto = matrixClient.getCrypto()!; const isCrossSigningReady = await crypto.isCrossSigningReady(); - if (isCrossSigningReady) setState("main"); - else setState("set_up_encryption"); + + // Check if the secrets are cached + const cachedSecrets = (await crypto.getCrossSigningStatus()).privateKeysCachedLocally; + const secretsOk = cachedSecrets.masterKey && cachedSecrets.selfSigningKey && cachedSecrets.userSigningKey; + + if (isCrossSigningReady && secretsOk) setState("main"); + else if (!isCrossSigningReady) setState("set_up_encryption"); + else setState("secrets_not_cached"); }, [matrixClient, setState]); // Initialise the state when the component is mounted useEffect(() => { - setUpEncryptionRequired(); - }, [setUpEncryptionRequired]); + checkEncryptionState(); + }, [checkEncryptionState]); // Also return the callback so that the component can re-run the logic. - return setUpEncryptionRequired; + return checkEncryptionState; } interface SetUpEncryptionPanelProps { diff --git a/test/unit-tests/components/views/settings/encryption/RecoveryPanel-test.tsx b/test/unit-tests/components/views/settings/encryption/RecoveryPanel-test.tsx index 6ef79876c7..d13e857954 100644 --- a/test/unit-tests/components/views/settings/encryption/RecoveryPanel-test.tsx +++ b/test/unit-tests/components/views/settings/encryption/RecoveryPanel-test.tsx @@ -10,22 +10,15 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { render, screen } from "jest-matrix-react"; import { waitFor } from "@testing-library/dom"; import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils"; import { RecoveryPanel } from "../../../../../../src/components/views/settings/encryption/RecoveryPanel"; -import { accessSecretStorage } from "../../../../../../src/SecurityManager"; - -jest.mock("../../../../../../src/SecurityManager", () => ({ - accessSecretStorage: jest.fn(), -})); describe("", () => { let matrixClient: MatrixClient; beforeEach(() => { matrixClient = createTestClient(); - mocked(accessSecretStorage).mockClear().mockResolvedValue(); }); function renderRecoverPanel(onChangeRecoveryKeyClick = jest.fn()) { @@ -56,18 +49,6 @@ describe("", () => { expect(onChangeRecoveryKeyClick).toHaveBeenCalledWith(true); }); - it("should ask to enter the recovery key when secrets are not cached", async () => { - jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key"); - const user = userEvent.setup(); - const { asFragment } = renderRecoverPanel(); - - await waitFor(() => screen.getByRole("button", { name: "Enter recovery key" })); - expect(asFragment()).toMatchSnapshot(); - - await user.click(screen.getByRole("button", { name: "Enter recovery key" })); - expect(accessSecretStorage).toHaveBeenCalled(); - }); - it("should allow to change the recovery key when everything is good", async () => { jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key"); jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({ diff --git a/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanel-test.tsx.snap b/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanel-test.tsx.snap index ff43b40677..d4d860d2cb 100644 --- a/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanel-test.tsx.snap +++ b/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanel-test.tsx.snap @@ -41,67 +41,6 @@ exports[` should allow to change the recovery key when everythi `; -exports[` should ask to enter the recovery key when secrets are not cached 1`] = ` - -

-
-

- Recovery -

-
- Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. - - - - - Your key storage is out of sync. Click the button below to fix the problem. - -
-
- -
- -`; - exports[` should ask to set up a recovery key when there is no recovery key 1`] = `
({ + accessSecretStorage: jest.fn(), +})); describe("", () => { let matrixClient: MatrixClient; @@ -33,6 +39,8 @@ describe("", () => { userSigningKey: true, }, }); + + mocked(accessSecretStorage).mockClear().mockResolvedValue(); }); function renderComponent() { @@ -68,6 +76,28 @@ describe("", () => { await waitFor(() => expect(screen.getByText("Recovery")).toBeInTheDocument()); }); + it("should ask to enter the recovery key when secrets are not cached", async () => { + // Secrets are not cached + jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({ + privateKeysInSecretStorage: true, + publicKeysOnDevice: true, + privateKeysCachedLocally: { + masterKey: false, + selfSigningKey: true, + userSigningKey: true, + }, + }); + + const user = userEvent.setup(); + const { asFragment } = renderComponent(); + + await waitFor(() => screen.getByRole("button", { name: "Enter recovery key" })); + expect(asFragment()).toMatchSnapshot(); + + await user.click(screen.getByRole("button", { name: "Enter recovery key" })); + expect(accessSecretStorage).toHaveBeenCalled(); + }); + it("should display the change recovery key panel when the user clicks on the change recovery button", async () => { const user = userEvent.setup(); diff --git a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap index b460b91e51..5856e6fda3 100644 --- a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap +++ b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/EncryptionUserSettingsTab-test.tsx.snap @@ -1,5 +1,75 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` should ask to enter the recovery key when secrets are not cached 1`] = ` + +
+
+
+
+

+ Recovery +

+
+ Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. + + + + + Your key storage is out of sync. Click the button below to fix the problem. + +
+
+ +
+
+
+
+`; + exports[` should display a verify button when the encryption is not set up 1`] = `