Handle cross-signing keys missing locally and/or from secret storage (#31367)
* show correct toast when cross-signing keys missing If cross-signing keys are missing both locally and in 4S, show a new toast saying that identity needs resetting, rather than saying that the device needs to be verified. * refactor: make DeviceListener in charge of device state - move enum from SetupEncryptionToast to DeviceListener - DeviceListener has public method to get device state - DeviceListener emits events to update device state * reset key backup when needed in RecoveryPanelOutOfSync brings RecoveryPanelOutOfSync in line with SetupEncryptionToast behaviour * update strings to agree with designs from Figma * use DeviceListener to determine EncryptionUserSettingsTab display rather than using its own logic * prompt to reset identity in Encryption Settings when needed * fix type * calculate device state even if we aren't going to show a toast * update snapshot * make logs more accurate * add tests * make the bot use a different access token/device * only log in a new session when requested * Mark properties as read-only Co-authored-by: Skye Elliot <actuallyori@gmail.com> * remove some duplicate strings * make accessToken optional instead of using empty string * switch from enum to string union as per review * apply other changes from review * handle errors in accessSecretStorage * remove incorrect testid --------- Co-authored-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
@@ -341,9 +341,7 @@ describe("DeviceListener", () => {
|
||||
await createAndStart();
|
||||
|
||||
expect(mockCrypto!.getUserDeviceInfo).toHaveBeenCalled();
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.VERIFY_THIS_SESSION,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("verify_this_session");
|
||||
});
|
||||
|
||||
describe("when current device is verified", () => {
|
||||
@@ -380,9 +378,23 @@ describe("DeviceListener", () => {
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.KEY_STORAGE_OUT_OF_SYNC,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("key_storage_out_of_sync");
|
||||
});
|
||||
|
||||
it("shows an identity reset toast when one of the cross-signing secrets is missing locally and in 4S", async () => {
|
||||
mockCrypto!.getCrossSigningStatus.mockResolvedValue({
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysInSecretStorage: false,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: false,
|
||||
selfSigningKey: true,
|
||||
userSigningKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("identity_needs_reset");
|
||||
});
|
||||
|
||||
it("shows an out-of-sync toast when the backup key is missing locally", async () => {
|
||||
@@ -392,9 +404,7 @@ describe("DeviceListener", () => {
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.KEY_STORAGE_OUT_OF_SYNC,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("key_storage_out_of_sync");
|
||||
});
|
||||
|
||||
it("does not show an out-of-sync toast when the backup key is missing locally but backup is purposely disabled", async () => {
|
||||
@@ -426,9 +436,7 @@ describe("DeviceListener", () => {
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.KEY_STORAGE_OUT_OF_SYNC,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("key_storage_out_of_sync");
|
||||
|
||||
// Then, when we receive the secret, it should be hidden.
|
||||
mockCrypto!.getCrossSigningStatus.mockResolvedValue({
|
||||
@@ -454,9 +462,7 @@ describe("DeviceListener", () => {
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.SET_UP_RECOVERY,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("set_up_recovery");
|
||||
});
|
||||
|
||||
it("shows an out-of-sync toast when one of the secrets is missing from 4S", async () => {
|
||||
@@ -470,9 +476,7 @@ describe("DeviceListener", () => {
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.KEY_STORAGE_OUT_OF_SYNC,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("key_storage_out_of_sync");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -573,9 +577,7 @@ describe("DeviceListener", () => {
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is displayed
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("turn_on_key_storage");
|
||||
});
|
||||
|
||||
it("shows the 'Turn on key storage' toast if we turned on key storage", async () => {
|
||||
@@ -591,9 +593,7 @@ describe("DeviceListener", () => {
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is displayed
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("turn_on_key_storage");
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
|
||||
@@ -606,9 +606,7 @@ describe("DeviceListener", () => {
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith("turn_on_key_storage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -626,9 +624,7 @@ describe("DeviceListener", () => {
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith("turn_on_key_storage");
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned on key storage", async () => {
|
||||
@@ -643,9 +639,7 @@ describe("DeviceListener", () => {
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith("turn_on_key_storage");
|
||||
});
|
||||
|
||||
it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => {
|
||||
@@ -661,9 +655,7 @@ describe("DeviceListener", () => {
|
||||
await createAndStart();
|
||||
|
||||
// Then the toast is not displayed
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith("turn_on_key_storage");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1206,25 +1198,21 @@ describe("DeviceListener", () => {
|
||||
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(SetupEncryptionToast.Kind.SET_UP_RECOVERY);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith("set_up_recovery");
|
||||
});
|
||||
|
||||
it("does not show the 'set up recovery' toast if secret storage is set up", async () => {
|
||||
mockCrypto!.getSecretStorageStatus.mockResolvedValue(readySecretStorageStatus);
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.SET_UP_RECOVERY,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith("set_up_recovery");
|
||||
});
|
||||
|
||||
it("does not show the 'set up recovery' toast if user has no encrypted rooms", async () => {
|
||||
jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.SET_UP_RECOVERY,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith("set_up_recovery");
|
||||
});
|
||||
|
||||
it("does not show the 'set up recovery' toast if the user has chosen to disable key storage", async () => {
|
||||
@@ -1236,9 +1224,7 @@ describe("DeviceListener", () => {
|
||||
});
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.SET_UP_RECOVERY,
|
||||
);
|
||||
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith("set_up_recovery");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,19 +9,45 @@ import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { RecoveryPanelOutOfSync } from "../../../../../../src/components/views/settings/encryption/RecoveryPanelOutOfSync";
|
||||
import { accessSecretStorage } from "../../../../../../src/SecurityManager";
|
||||
import { AccessCancelledError, accessSecretStorage } from "../../../../../../src/SecurityManager";
|
||||
import DeviceListener from "../../../../../../src/DeviceListener";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
|
||||
jest.mock("../../../../../../src/SecurityManager", () => ({
|
||||
accessSecretStorage: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../../../../src/SecurityManager", () => {
|
||||
const originalModule = jest.requireActual("../../../../../../src/SecurityManager");
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
accessSecretStorage: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("<RecoveyPanelOutOfSync />", () => {
|
||||
function renderComponent(onFinish = jest.fn(), onForgotRecoveryKey = jest.fn()) {
|
||||
return render(<RecoveryPanelOutOfSync onFinish={onFinish} onForgotRecoveryKey={onForgotRecoveryKey} />);
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
function renderComponent(
|
||||
onFinish = jest.fn(),
|
||||
onForgotRecoveryKey = jest.fn(),
|
||||
onAccessSecretStorageFailed = jest.fn(),
|
||||
) {
|
||||
matrixClient = createTestClient();
|
||||
return render(
|
||||
<RecoveryPanelOutOfSync
|
||||
onFinish={onFinish}
|
||||
onForgotRecoveryKey={onForgotRecoveryKey}
|
||||
onAccessSecretStorageFailed={onAccessSecretStorageFailed}
|
||||
/>,
|
||||
withClientContextRenderOptions(matrixClient),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const { asFragment } = renderComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
@@ -38,8 +64,12 @@ describe("<RecoveyPanelOutOfSync />", () => {
|
||||
});
|
||||
|
||||
it("should access to 4S and call onFinish when 'Enter recovery key' is clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(false);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
return await func();
|
||||
});
|
||||
|
||||
const onFinish = jest.fn();
|
||||
renderComponent(onFinish);
|
||||
@@ -47,5 +77,59 @@ describe("<RecoveyPanelOutOfSync />", () => {
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onFinish).toHaveBeenCalled();
|
||||
|
||||
expect(matrixClient.getCrypto()!.resetKeyBackup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should reset key backup if needed", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
return await func();
|
||||
});
|
||||
|
||||
const onFinish = jest.fn();
|
||||
renderComponent(onFinish);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onFinish).toHaveBeenCalled();
|
||||
|
||||
expect(matrixClient.getCrypto()!.resetKeyBackup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onAccessSecretStorageFailed on failure", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
throw new Error("Error");
|
||||
});
|
||||
|
||||
const onAccessSecretStorageFailed = jest.fn();
|
||||
renderComponent(jest.fn(), jest.fn(), onAccessSecretStorageFailed);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onAccessSecretStorageFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not call onAccessSecretStorageFailed when cancelled", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsBackupReset").mockResolvedValue(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
mocked(accessSecretStorage).mockImplementation(async (func = async (): Promise<void> => {}) => {
|
||||
throw new AccessCancelledError();
|
||||
});
|
||||
|
||||
const onFinish = jest.fn();
|
||||
const onAccessSecretStorageFailed = jest.fn();
|
||||
renderComponent(onFinish, jest.fn(), onAccessSecretStorageFailed);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||
expect(accessSecretStorage).toHaveBeenCalled();
|
||||
expect(onFinish).not.toHaveBeenCalled();
|
||||
expect(onAccessSecretStorageFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import React from "react";
|
||||
import { act, render, screen } from "jest-matrix-react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
} from "../../../../../../../src/components/views/settings/tabs/user/EncryptionUserSettingsTab";
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||
import Modal from "../../../../../../../src/Modal";
|
||||
import DeviceListener from "../../../../../../../src/DeviceListener";
|
||||
|
||||
describe("<EncryptionUserSettingsTab />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
@@ -37,22 +39,21 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
userSigningKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "getDeviceState").mockReturnValue("ok");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function renderComponent(props: { initialState?: State } = {}) {
|
||||
return render(<EncryptionUserSettingsTab {...props} />, withClientContextRenderOptions(matrixClient));
|
||||
}
|
||||
|
||||
it("should display a loading state when the encryption state is computed", () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "isCrossSigningReady").mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
renderComponent();
|
||||
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a verify button when the encryption is not set up", async () => {
|
||||
const user = userEvent.setup();
|
||||
jest.spyOn(matrixClient.getCrypto()!, "isCrossSigningReady").mockResolvedValue(false);
|
||||
mocked(DeviceListener.sharedInstance().getDeviceState).mockReturnValue("verify_this_session");
|
||||
|
||||
const { asFragment } = renderComponent();
|
||||
await waitFor(() =>
|
||||
@@ -81,17 +82,7 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
});
|
||||
|
||||
it("should display the recovery out of sync panel when secrets are not cached", async () => {
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("1");
|
||||
// Secrets are not cached
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
|
||||
privateKeysInSecretStorage: true,
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: false,
|
||||
selfSigningKey: true,
|
||||
userSigningKey: true,
|
||||
},
|
||||
});
|
||||
mocked(DeviceListener.sharedInstance().getDeviceState).mockReturnValue("key_storage_out_of_sync");
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { asFragment } = renderComponent();
|
||||
@@ -196,18 +187,7 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
it("should re-check the encryption state and displays the correct panel when the user clicks cancel the reset identity flow", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("1");
|
||||
|
||||
// Secrets are not cached
|
||||
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
|
||||
privateKeysInSecretStorage: true,
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: false,
|
||||
selfSigningKey: true,
|
||||
userSigningKey: true,
|
||||
},
|
||||
});
|
||||
mocked(DeviceListener.sharedInstance().getDeviceState).mockReturnValue("key_storage_out_of_sync");
|
||||
|
||||
renderComponent({ initialState: "reset_identity_forgot" });
|
||||
|
||||
@@ -220,4 +200,17 @@ describe("<EncryptionUserSettingsTab />", () => {
|
||||
screen.getByText("Your key storage is out of sync. Click one of the buttons below to fix the problem."),
|
||||
);
|
||||
});
|
||||
|
||||
it("should display the identity needs reset panel when the user's identity needs resetting", async () => {
|
||||
mocked(DeviceListener.sharedInstance().getDeviceState).mockReturnValue("identity_needs_reset");
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { asFragment } = renderComponent();
|
||||
|
||||
await waitFor(() => screen.getByRole("button", { name: "Continue with reset" }));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Continue with reset" }));
|
||||
expect(screen.getByRole("heading", { name: "You need to reset your identity" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,6 +81,64 @@ exports[`<EncryptionUserSettingsTab /> should display the change recovery key pa
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EncryptionUserSettingsTab /> should display the identity needs reset panel when the user's identity needs resetting 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SettingsTab mx_EncryptionUserSettingsTab"
|
||||
data-testid="encryptionTab"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsTab_sections"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSection_header"
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 mx_SettingsHeader"
|
||||
>
|
||||
Your key storage is out of sync.
|
||||
</h2>
|
||||
<div
|
||||
class="mx_SettingsSubheader"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsSubheader_error"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
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 22"
|
||||
/>
|
||||
</svg>
|
||||
You have to reset your cryptographic identity in order to ensure access to your message history
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="primary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue with reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EncryptionUserSettingsTab /> should display the recovery out of sync panel when secrets are not cached 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
|
||||
@@ -15,11 +15,12 @@ import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import * as SecurityManager from "../../../src/SecurityManager";
|
||||
import ToastContainer from "../../../src/components/structures/ToastContainer";
|
||||
import { Kind, showToast } from "../../../src/toasts/SetupEncryptionToast";
|
||||
import { 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";
|
||||
import SetupEncryptionDialog from "../../../src/components/views/dialogs/security/SetupEncryptionDialog";
|
||||
import { stubClient } from "../../test-utils";
|
||||
|
||||
jest.mock("../../../src/dispatcher/dispatcher", () => ({
|
||||
@@ -36,7 +37,7 @@ describe("SetupEncryptionToast", () => {
|
||||
|
||||
describe("Set up recovery", () => {
|
||||
it("should render the toast", async () => {
|
||||
act(() => showToast(Kind.SET_UP_RECOVERY));
|
||||
act(() => showToast("set_up_recovery"));
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Set up recovery" })).toBeInTheDocument();
|
||||
});
|
||||
@@ -45,7 +46,7 @@ describe("SetupEncryptionToast", () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "recordRecoveryDisabled");
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "dismissEncryptionSetup");
|
||||
|
||||
act(() => showToast(Kind.SET_UP_RECOVERY));
|
||||
act(() => showToast("set_up_recovery"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Dismiss" }));
|
||||
@@ -55,14 +56,6 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Verify this session", () => {
|
||||
it("should render the toast", async () => {
|
||||
act(() => showToast(Kind.VERIFY_THIS_SESSION));
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Verify this session" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Key storage out of sync", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
|
||||
@@ -77,13 +70,13 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
|
||||
it("should render the toast", async () => {
|
||||
act(() => showToast(Kind.KEY_STORAGE_OUT_OF_SYNC));
|
||||
act(() => showToast("key_storage_out_of_sync"));
|
||||
|
||||
await expect(screen.findByText("Your key storage is out of sync.")).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should reset key backup if needed", async () => {
|
||||
showToast(Kind.KEY_STORAGE_OUT_OF_SYNC);
|
||||
showToast("key_storage_out_of_sync");
|
||||
|
||||
jest.spyOn(SecurityManager, "accessSecretStorage").mockImplementation(
|
||||
async (func = async (): Promise<void> => {}) => {
|
||||
@@ -100,7 +93,7 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
|
||||
it("should not reset key backup if not needed", async () => {
|
||||
showToast(Kind.KEY_STORAGE_OUT_OF_SYNC);
|
||||
showToast("key_storage_out_of_sync");
|
||||
|
||||
jest.spyOn(SecurityManager, "accessSecretStorage").mockImplementation(
|
||||
async (func = async (): Promise<void> => {}) => {
|
||||
@@ -122,7 +115,7 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
|
||||
it("should open settings to the reset flow when 'forgot recovery key' clicked and identity reset needed", async () => {
|
||||
act(() => showToast(Kind.KEY_STORAGE_OUT_OF_SYNC));
|
||||
act(() => showToast("key_storage_out_of_sync"));
|
||||
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsCrossSigningReset").mockResolvedValue(
|
||||
true,
|
||||
@@ -139,7 +132,7 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
|
||||
it("should open settings to the change recovery key flow when 'forgot recovery key' clicked and identity reset not needed", async () => {
|
||||
act(() => showToast(Kind.KEY_STORAGE_OUT_OF_SYNC));
|
||||
act(() => showToast("key_storage_out_of_sync"));
|
||||
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "keyStorageOutOfSyncNeedsCrossSigningReset").mockResolvedValue(
|
||||
false,
|
||||
@@ -164,7 +157,7 @@ describe("SetupEncryptionToast", () => {
|
||||
true,
|
||||
);
|
||||
|
||||
act(() => showToast(Kind.KEY_STORAGE_OUT_OF_SYNC));
|
||||
act(() => showToast("key_storage_out_of_sync"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByText("Enter recovery key"));
|
||||
@@ -185,7 +178,7 @@ describe("SetupEncryptionToast", () => {
|
||||
false,
|
||||
);
|
||||
|
||||
act(() => showToast(Kind.KEY_STORAGE_OUT_OF_SYNC));
|
||||
act(() => showToast("key_storage_out_of_sync"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByText("Enter recovery key"));
|
||||
@@ -200,7 +193,7 @@ describe("SetupEncryptionToast", () => {
|
||||
|
||||
describe("Turn on key storage", () => {
|
||||
it("should render the toast", async () => {
|
||||
act(() => showToast(Kind.TURN_ON_KEY_STORAGE));
|
||||
act(() => showToast("turn_on_key_storage"));
|
||||
|
||||
await expect(screen.findByText("Turn on key storage")).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Dismiss" })).resolves.toBeInTheDocument();
|
||||
@@ -210,7 +203,7 @@ describe("SetupEncryptionToast", () => {
|
||||
it("should open settings to the Encryption tab when 'Continue' clicked", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "recordKeyBackupDisabled");
|
||||
|
||||
act(() => showToast(Kind.TURN_ON_KEY_STORAGE));
|
||||
act(() => showToast("turn_on_key_storage"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Continue" }));
|
||||
@@ -232,7 +225,7 @@ describe("SetupEncryptionToast", () => {
|
||||
});
|
||||
|
||||
// When we show the toast, and click Dismiss
|
||||
act(() => showToast(Kind.TURN_ON_KEY_STORAGE));
|
||||
act(() => showToast("turn_on_key_storage"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Dismiss" }));
|
||||
@@ -248,4 +241,65 @@ describe("SetupEncryptionToast", () => {
|
||||
expect(DeviceListener.sharedInstance().recordKeyBackupDisabled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Verify this session", () => {
|
||||
it("should render the toast", async () => {
|
||||
act(() => showToast("verify_this_session"));
|
||||
|
||||
await expect(screen.findByText("Verify this session")).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Later" })).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Verify" })).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should dismiss the toast when 'Later' button clicked, and remember it", async () => {
|
||||
jest.spyOn(DeviceListener.sharedInstance(), "dismissEncryptionSetup");
|
||||
|
||||
act(() => showToast("verify_this_session"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Later" }));
|
||||
|
||||
expect(DeviceListener.sharedInstance().dismissEncryptionSetup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should open the verification dialog when 'Verify' clicked", async () => {
|
||||
jest.spyOn(Modal, "createDialog");
|
||||
|
||||
// When we show the toast, and click Verify
|
||||
act(() => showToast("verify_this_session"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: "Verify" }));
|
||||
|
||||
// Then the dialog was opened
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(SetupEncryptionDialog, {}, undefined, false, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Identity needs reset", () => {
|
||||
it("should render the toast", async () => {
|
||||
act(() => showToast("identity_needs_reset"));
|
||||
|
||||
await expect(screen.findByText("Your key storage is out of sync.")).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(
|
||||
"You have to reset your cryptographic identity in order to ensure access to your message history",
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByRole("button", { name: "Continue with reset" })).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open settings to the reset flow when 'Continue with reset' clicked", async () => {
|
||||
act(() => showToast("identity_needs_reset"));
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByText("Continue with reset"));
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_user_settings",
|
||||
initialTabId: "USER_ENCRYPTION_TAB",
|
||||
props: { initialEncryptionState: "reset_identity_cant_recover" },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user