/* Copyright 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { render, RenderResult, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import { mocked, MockedObject } from "jest-mock"; import { CryptoApi, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils"; import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { filterConsole, flushPromises, getMockClientWithEventEmitter, mockClientMethodsCrypto, mockClientMethodsServer, } from "../../../../test-utils"; import CreateSecretStorageDialog from "../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog"; import Modal from "../../../../../src/Modal"; import RestoreKeyBackupDialog from "../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog"; describe("CreateSecretStorageDialog", () => { let mockClient: MockedObject; let mockCrypto: MockedObject; beforeEach(() => { mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsServer(), ...mockClientMethodsCrypto(), uploadDeviceSigningKeys: jest.fn().mockImplementation(async () => { await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately throw new MatrixError({ flows: [] }); }), }); mockCrypto = mocked(mockClient.getCrypto()!); Object.assign(mockCrypto, { isKeyBackupTrusted: jest.fn(), isDehydrationSupported: jest.fn(() => false), bootstrapCrossSigning: jest.fn(), bootstrapSecretStorage: jest.fn(), }); }); afterEach(() => { jest.restoreAllMocks(); }); function renderComponent( props: Partial> = {}, ): RenderResult { const onFinished = jest.fn(); return render(); } it("shows a loading spinner initially", async () => { const { container } = renderComponent(); expect(screen.getByTestId("spinner")).toBeDefined(); expect(container).toMatchSnapshot(); await flushPromises(); }); describe("when there is an error fetching the backup version", () => { filterConsole("Error fetching backup data from server"); it("shows an error", async () => { mockClient.getKeyBackupVersion.mockImplementation(async () => { throw new Error("bleh bleh"); }); const result = renderComponent(); // XXX the error message is... misleading. await result.findByText("Unable to query secret storage status"); expect(result.container).toMatchSnapshot(); }); }); it("shows 'Generate a Security Key' text if no key backup is present", async () => { const result = renderComponent(); await flushPromises(); expect(result.container).toMatchSnapshot(); result.getByText("Generate a Security Key"); }); describe("when canUploadKeysWithPasswordOnly", () => { // spy on Modal.createDialog let modalSpy: jest.SpyInstance; // deferred which should be resolved to indicate that the created dialog has completed let restoreDialogFinishedDefer: IDeferred<[done?: boolean]>; beforeEach(() => { mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); mockClient.uploadDeviceSigningKeys.mockImplementation(async () => { await sleep(0); throw new MatrixError({ flows: [{ stages: ["m.login.password"] }], }); }); restoreDialogFinishedDefer = defer<[done?: boolean]>(); modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: restoreDialogFinishedDefer.promise, close: jest.fn(), }); }); it("prompts for a password and then shows RestoreKeyBackupDialog", async () => { const result = renderComponent(); await result.findByText(/Enter your account password to confirm the upgrade/); expect(result.container).toMatchSnapshot(); await userEvent.type(result.getByPlaceholderText("Password"), "my pass"); result.getByRole("button", { name: "Next" }).click(); expect(modalSpy).toHaveBeenCalledWith( RestoreKeyBackupDialog, { keyCallback: expect.any(Function), showSummary: false, }, undefined, false, false, ); restoreDialogFinishedDefer.resolve([]); }); it("calls bootstrapSecretStorage once keys are restored if the backup is now trusted", async () => { mockClient.isCryptoEnabled.mockReturnValue(true); const result = renderComponent(); await result.findByText(/Enter your account password to confirm the upgrade/); expect(result.container).toMatchSnapshot(); await userEvent.type(result.getByPlaceholderText("Password"), "my pass"); result.getByRole("button", { name: "Next" }).click(); expect(modalSpy).toHaveBeenCalled(); // While we restore the key backup, its signature becomes accepted mockCrypto.isKeyBackupTrusted.mockResolvedValue({ trusted: true } as BackupTrustInfo); restoreDialogFinishedDefer.resolve([]); await flushPromises(); // XXX no idea why this is a sensible thing to do. I just work here. expect(mockCrypto.bootstrapCrossSigning).toHaveBeenCalled(); expect(mockCrypto.bootstrapSecretStorage).toHaveBeenCalled(); await result.findByText("Your keys are now being backed up from this device."); }); describe("when there is an error fetching the backup version after RestoreKeyBackupDialog", () => { filterConsole("Error fetching backup data from server"); it("handles the error sensibly", async () => { const result = renderComponent(); await result.findByText(/Enter your account password to confirm the upgrade/); expect(result.container).toMatchSnapshot(); await userEvent.type(result.getByPlaceholderText("Password"), "my pass"); result.getByRole("button", { name: "Next" }).click(); expect(modalSpy).toHaveBeenCalled(); mockClient.getKeyBackupVersion.mockImplementation(async () => { throw new Error("bleh bleh"); }); restoreDialogFinishedDefer.resolve([]); await result.findByText("Unable to query secret storage status"); }); }); }); describe("when backup is present but not trusted", () => { beforeEach(() => { mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); }); it("shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked", async () => { const result = renderComponent(); await result.findByText("Restore your key backup to upgrade your encryption"); expect(result.container).toMatchSnapshot(); // before we click "Restore", set up a spy on createDialog const restoreDialogFinishedDefer = defer<[done?: boolean]>(); const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: restoreDialogFinishedDefer.promise, close: jest.fn(), }); result.getByRole("button", { name: "Restore" }).click(); expect(modalSpy).toHaveBeenCalledWith( RestoreKeyBackupDialog, { keyCallback: expect.any(Function), showSummary: false, }, undefined, false, false, ); // simulate RestoreKeyBackupDialog completing, to run that code path restoreDialogFinishedDefer.resolve([]); }); }); });