Prompt the user when key storage is unexpectedly off (#29912)
* Assert that we set backup_disabled when turning off key storage * Prompt the user when key storage is unexpectedly off * Playwright tests for the Turn on key storage toast
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
|
||||
|
||||
import DeviceListener from "../../src/DeviceListener";
|
||||
import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../src/DeviceListener";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import * as SetupEncryptionToast from "../../src/toasts/SetupEncryptionToast";
|
||||
import * as UnverifiedSessionToast from "../../src/toasts/UnverifiedSessionToast";
|
||||
@@ -118,6 +118,7 @@ describe("DeviceListener", () => {
|
||||
getDeviceId: jest.fn().mockReturnValue(deviceId),
|
||||
setAccountData: jest.fn(),
|
||||
getAccountData: jest.fn(),
|
||||
getAccountDataFromServer: jest.fn(),
|
||||
deleteAccountData: jest.fn(),
|
||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
||||
secretStorage: {
|
||||
@@ -309,6 +310,8 @@ describe("DeviceListener", () => {
|
||||
it("hides setup encryption toast when cross signing and secret storage are ready", async () => {
|
||||
mockCrypto!.isCrossSigningReady.mockResolvedValue(true);
|
||||
mockCrypto!.isSecretStorageReady.mockResolvedValue(true);
|
||||
mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
|
||||
await createAndStart();
|
||||
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
|
||||
});
|
||||
@@ -377,6 +380,7 @@ describe("DeviceListener", () => {
|
||||
|
||||
it("hides the out-of-sync toast when one of the secrets is missing", async () => {
|
||||
mockCrypto!.isSecretStorageReady.mockResolvedValue(true);
|
||||
mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
|
||||
// First show the toast
|
||||
mockCrypto!.getCrossSigningStatus.mockResolvedValue({
|
||||
@@ -414,6 +418,7 @@ describe("DeviceListener", () => {
|
||||
it("shows set up recovery toast when user has a key backup available", async () => {
|
||||
// non falsy response
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
mockClient.secretStorage.getDefaultKeyId.mockResolvedValue(null);
|
||||
|
||||
await createAndStart();
|
||||
@@ -444,6 +449,9 @@ describe("DeviceListener", () => {
|
||||
|
||||
it("dispatches keybackup event when key backup is not enabled", async () => {
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue(null);
|
||||
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
|
||||
);
|
||||
await createAndStart();
|
||||
expect(mockDispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ReportKeyBackupNotEnabled,
|
||||
@@ -463,6 +471,137 @@ describe("DeviceListener", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sets backup_disabled account data when we call recordKeyBackupDisabled", async () => {
|
||||
const instance = await createAndStart();
|
||||
await instance.recordKeyBackupDisabled();
|
||||
|
||||
expect(mockClient.setAccountData).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when crypto is in use and set up", () => {
|
||||
beforeEach(() => {
|
||||
// Encryption is in use
|
||||
mockClient.getRooms.mockReturnValue([{ roomId: "!room1" }, { roomId: "!room2" }] as unknown as Room[]);
|
||||
jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
|
||||
// The device is verified
|
||||
mockCrypto.getDeviceVerificationStatus.mockResolvedValue(
|
||||
new DeviceVerificationStatus({ crossSigningVerified: true }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("but key storage is off", () => {
|
||||
beforeEach(() => {
|
||||
// There is no active key backup/storage
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("shows the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => {
|
||||
// Given key backup is off but the account data saying we turned it off is not set
|
||||
// (m.org.matrix.custom.backup_disabled)
|
||||
mockClient.getAccountData.mockReturnValue(undefined);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is displayed
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the 'Turn on key storage' toast if we turned on key storage", async () => {
|
||||
// Given key backup is off but the account data says we turned it on (this should not happen - the
|
||||
// account data should only be updated if we turn on key storage)
|
||||
mockClient.getAccountData.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
|
||||
? new MatrixEvent({ content: { disabled: false } })
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is displayed
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
|
||||
// Given key backup is off but the account data saying we turned it off is set
|
||||
mockClient.getAccountDataFromServer.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and key storage is on", () => {
|
||||
beforeEach(() => {
|
||||
// There is an active key backup/storage
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => {
|
||||
// Given key backup is on and the account data saying we turned it off is not set
|
||||
mockClient.getAccountData.mockReturnValue(undefined);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned on key storage", async () => {
|
||||
// Given key backup is on and the account data says we turned it on
|
||||
mockClient.getAccountData.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
|
||||
? new MatrixEvent({ content: { disabled: false } })
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
|
||||
// Given key backup is on but the account data saying we turned it off is set (this should never
|
||||
// happen - it should only be set when we turn off key storage or dismiss the toast)
|
||||
mockClient.getAccountData.mockImplementation((eventType) =>
|
||||
eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY
|
||||
? new MatrixEvent({ content: { disabled: true } })
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// When we launch the DeviceListener
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("unverified sessions toasts", () => {
|
||||
const currentDevice = new Device({ deviceId, userId: userId, algorithms: [], keys: new Map() });
|
||||
const device2 = new Device({ deviceId: "d2", userId: userId, algorithms: [], keys: new Map() });
|
||||
@@ -997,6 +1136,8 @@ describe("DeviceListener", () => {
|
||||
});
|
||||
|
||||
it("shows the 'set up recovery' toast if user has not set up 4S", async () => {
|
||||
mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(SetupEncryptionToast.Kind.SET_UP_RECOVERY);
|
||||
|
||||
@@ -87,5 +87,8 @@ describe("KeyStoragePanelViewModel", () => {
|
||||
await result.current.setEnabled(false);
|
||||
|
||||
expect(mocked(matrixClient.getCrypto()!.disableKeyStorage)).toHaveBeenCalled();
|
||||
expect(mocked(matrixClient.setAccountData)).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", {
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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 React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import ConfirmKeyStorageOffDialog from "../../../../../src/components/views/dialogs/ConfirmKeyStorageOffDialog";
|
||||
|
||||
describe("ConfirmKeyStorageOffDialog", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const dialog = render(<ConfirmKeyStorageOffDialog onFinished={jest.fn()} />);
|
||||
expect(dialog.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onFinished with dismissed=true if we dismiss", () => {
|
||||
const onFinished = jest.fn();
|
||||
const dialog = render(<ConfirmKeyStorageOffDialog onFinished={onFinished} />);
|
||||
|
||||
dialog.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("calls onFinished with dismissed=true if we continue", () => {
|
||||
const onFinished = jest.fn();
|
||||
const dialog = render(<ConfirmKeyStorageOffDialog onFinished={onFinished} />);
|
||||
|
||||
dialog.getByRole("button", { name: "Go to Settings" }).click();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmKeyStorageOffDialog renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_EncryptionCard"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_content_o77nw_8 _destructive_o77nw_34"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Are you sure you want to keep key storage turned off?
|
||||
</h2>
|
||||
</div>
|
||||
<span>
|
||||
If you sign out of all your devices you will lose your message history and will need to verify all your existing contacts again.
|
||||
<br />
|
||||
<a
|
||||
href="https://element.io/help#encryption5"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Go to Settings
|
||||
</button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Yes, dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -14,6 +14,8 @@ import ToastContainer from "../../../src/components/structures/ToastContainer";
|
||||
import { Kind, showToast } from "../../../src/toasts/SetupEncryptionToast";
|
||||
import dis from "../../../src/dispatcher/dispatcher";
|
||||
import DeviceListener from "../../../src/DeviceListener";
|
||||
import Modal from "../../../src/Modal";
|
||||
import ConfirmKeyStorageOffDialog from "../../../src/components/views/dialogs/ConfirmKeyStorageOffDialog";
|
||||
|
||||
jest.mock("../../../src/dispatcher/dispatcher", () => ({
|
||||
dispatch: jest.fn(),
|
||||
@@ -83,4 +85,55 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Turn on key storage", () => {
|
||||
it("should render the toast", async () => {
|
||||
showToast(Kind.TURN_ON_KEY_STORAGE);
|
||||
|
||||
await expect(screen.findByText("Turn on key storage")).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Dismiss" })).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Continue" })).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open settings to the Encryption tab when 'Continue' clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "recordKeyBackupDisabled");
|
||||
|
||||
showToast(Kind.TURN_ON_KEY_STORAGE);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Continue" }));
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_user_settings",
|
||||
initialTabId: "USER_ENCRYPTION_TAB",
|
||||
});
|
||||
|
||||
expect(DeviceListener.sharedInstance().recordKeyBackupDisabled).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should open the confirm key storage off dialog when 'Dismiss' clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "recordKeyBackupDisabled");
|
||||
|
||||
// Given that as soon as the dialog opens, it closes and says "yes they clicked dismiss"
|
||||
jest.spyOn(Modal, "createDialog").mockImplementation(() => {
|
||||
return { finished: Promise.resolve([true]) } as any;
|
||||
});
|
||||
|
||||
// When we show the toast, and click Dismiss
|
||||
showToast(Kind.TURN_ON_KEY_STORAGE);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Dismiss" }));
|
||||
|
||||
// Then the dialog was opened
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(
|
||||
ConfirmKeyStorageOffDialog,
|
||||
undefined,
|
||||
"mx_ConfirmKeyStorageOffDialog",
|
||||
);
|
||||
|
||||
// And the backup was disabled when the dialog's onFinished was called
|
||||
expect(DeviceListener.sharedInstance().recordKeyBackupDisabled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user