CreateSecretStorageDialog
: stop using deprecated APIs (#11635)
* Add some tests for `CreateSecretStorageDialog` * CreateSecretStorageDialog: stop using deprecated APIs
This commit is contained in:
parent
11f258e62e
commit
6fd46f3bc8
5 changed files with 830 additions and 28 deletions
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -18,12 +18,11 @@ limitations under the License.
|
||||||
import React, { createRef } from "react";
|
import React, { createRef } from "react";
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
||||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
|
||||||
import { CrossSigningKeys, IAuthDict, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api";
|
import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import { _t, _td } from "../../../../languageHandler";
|
import { _t, _td } from "../../../../languageHandler";
|
||||||
|
@ -81,8 +80,25 @@ interface IState {
|
||||||
copied: boolean;
|
copied: boolean;
|
||||||
downloaded: boolean;
|
downloaded: boolean;
|
||||||
setPassphrase: boolean;
|
setPassphrase: boolean;
|
||||||
backupInfo: IKeyBackupInfo | null;
|
|
||||||
backupSigStatus: TrustInfo | null;
|
/** Information on the current key backup version, as returned by the server.
|
||||||
|
*
|
||||||
|
* `null` could mean any of:
|
||||||
|
* * we haven't yet requested the data from the server.
|
||||||
|
* * we were unable to reach the server.
|
||||||
|
* * the server returned key backup version data we didn't understand or was malformed.
|
||||||
|
* * there is actually no backup on the server.
|
||||||
|
*/
|
||||||
|
backupInfo: KeyBackupInfo | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to
|
||||||
|
* decrypt it.
|
||||||
|
*
|
||||||
|
* `undefined` if `backupInfo` is null, or if crypto is not enabled in the client.
|
||||||
|
*/
|
||||||
|
backupTrustInfo: BackupTrustInfo | undefined;
|
||||||
|
|
||||||
// does the server offer a UI auth flow with just m.login.password
|
// does the server offer a UI auth flow with just m.login.password
|
||||||
// for /keys/device_signing/upload?
|
// for /keys/device_signing/upload?
|
||||||
canUploadKeysWithPasswordOnly: boolean | null;
|
canUploadKeysWithPasswordOnly: boolean | null;
|
||||||
|
@ -144,7 +160,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
setPassphrase: false,
|
setPassphrase: false,
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
backupSigStatus: null,
|
backupTrustInfo: undefined,
|
||||||
// does the server offer a UI auth flow with just m.login.password
|
// does the server offer a UI auth flow with just m.login.password
|
||||||
// for /keys/device_signing/upload?
|
// for /keys/device_signing/upload?
|
||||||
accountPasswordCorrect: null,
|
accountPasswordCorrect: null,
|
||||||
|
@ -177,13 +193,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
this.fetchBackupInfo();
|
this.fetchBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchBackupInfo(): Promise<{ backupInfo?: IKeyBackupInfo; backupSigStatus?: TrustInfo }> {
|
/**
|
||||||
|
* Attempt to get information on the current backup from the server, and update the state.
|
||||||
|
*
|
||||||
|
* Updates {@link IState.backupInfo} and {@link IState.backupTrustInfo}, and picks an appropriate phase for
|
||||||
|
* {@link IState.phase}.
|
||||||
|
*
|
||||||
|
* @returns If the backup data was retrieved successfully, the trust info for the backup. Otherwise, undefined.
|
||||||
|
*/
|
||||||
|
private async fetchBackupInfo(): Promise<BackupTrustInfo | undefined> {
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const backupInfo = await cli.getKeyBackupVersion();
|
const backupInfo = await cli.getKeyBackupVersion();
|
||||||
const backupSigStatus =
|
const backupTrustInfo =
|
||||||
// we may not have started crypto yet, in which case we definitely don't trust the backup
|
// we may not have started crypto yet, in which case we definitely don't trust the backup
|
||||||
backupInfo && cli.isCryptoEnabled() ? await cli.isKeyBackupTrusted(backupInfo) : null;
|
backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
||||||
|
|
||||||
const { forceReset } = this.props;
|
const { forceReset } = this.props;
|
||||||
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
|
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
|
||||||
|
@ -191,16 +215,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
this.setState({
|
this.setState({
|
||||||
phase,
|
phase,
|
||||||
backupInfo,
|
backupInfo,
|
||||||
backupSigStatus,
|
backupTrustInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return backupTrustInfo;
|
||||||
backupInfo: backupInfo ?? undefined,
|
|
||||||
backupSigStatus: backupSigStatus ?? undefined,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error("Error fetching backup data from server", e);
|
||||||
this.setState({ phase: Phase.LoadError });
|
this.setState({ phase: Phase.LoadError });
|
||||||
return {};
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,7 +259,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
|
|
||||||
private onChooseKeyPassphraseFormSubmit = async (): Promise<void> => {
|
private onChooseKeyPassphraseFormSubmit = async (): Promise<void> => {
|
||||||
if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
|
if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
|
||||||
this.recoveryKey = await MatrixClientPeg.safeGet().createRecoveryKeyFromPassphrase();
|
this.recoveryKey = await MatrixClientPeg.safeGet().getCrypto()!.createRecoveryKeyFromPassphrase();
|
||||||
this.setState({
|
this.setState({
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
|
@ -255,7 +277,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
|
|
||||||
private onMigrateFormSubmit = (e: React.FormEvent): void => {
|
private onMigrateFormSubmit = (e: React.FormEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.state.backupSigStatus?.usable) {
|
if (this.state.backupTrustInfo?.trusted) {
|
||||||
this.bootstrapSecretStorage();
|
this.bootstrapSecretStorage();
|
||||||
} else {
|
} else {
|
||||||
this.restoreBackup();
|
this.restoreBackup();
|
||||||
|
@ -284,7 +306,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
};
|
};
|
||||||
|
|
||||||
private doBootstrapUIAuth = async (
|
private doBootstrapUIAuth = async (
|
||||||
makeRequest: (authData: IAuthDict) => Promise<UIAResponse<void>>,
|
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||||
await makeRequest({
|
await makeRequest({
|
||||||
|
@ -337,13 +359,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
});
|
});
|
||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
|
const crypto = cli.getCrypto()!;
|
||||||
|
|
||||||
const { forceReset } = this.props;
|
const { forceReset } = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (forceReset) {
|
if (forceReset) {
|
||||||
logger.log("Forcing secret storage reset");
|
logger.log("Forcing secret storage reset");
|
||||||
await cli.bootstrapSecretStorage({
|
await crypto.bootstrapSecretStorage({
|
||||||
createSecretStorageKey: async () => this.recoveryKey!,
|
createSecretStorageKey: async () => this.recoveryKey!,
|
||||||
setupNewKeyBackup: true,
|
setupNewKeyBackup: true,
|
||||||
setupNewSecretStorage: true,
|
setupNewSecretStorage: true,
|
||||||
|
@ -356,10 +379,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
// * SSO authentication users which require interactive auth to upload
|
// * SSO authentication users which require interactive auth to upload
|
||||||
// keys (and also happen to skip all post-authentication flows at the
|
// keys (and also happen to skip all post-authentication flows at the
|
||||||
// moment via token login)
|
// moment via token login)
|
||||||
await cli.bootstrapCrossSigning({
|
await crypto.bootstrapCrossSigning({
|
||||||
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||||
});
|
});
|
||||||
await cli.bootstrapSecretStorage({
|
await crypto.bootstrapSecretStorage({
|
||||||
createSecretStorageKey: async () => this.recoveryKey!,
|
createSecretStorageKey: async () => this.recoveryKey!,
|
||||||
keyBackupInfo: this.state.backupInfo!,
|
keyBackupInfo: this.state.backupInfo!,
|
||||||
setupNewKeyBackup: !this.state.backupInfo,
|
setupNewKeyBackup: !this.state.backupInfo,
|
||||||
|
@ -420,8 +443,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
);
|
);
|
||||||
|
|
||||||
await finished;
|
await finished;
|
||||||
const { backupSigStatus } = await this.fetchBackupInfo();
|
const backupTrustInfo = await this.fetchBackupInfo();
|
||||||
if (backupSigStatus?.usable && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||||
this.bootstrapSecretStorage();
|
this.bootstrapSecretStorage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -462,7 +485,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
|
|
||||||
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||||
|
|
||||||
this.recoveryKey = await MatrixClientPeg.safeGet().createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
this.recoveryKey = await MatrixClientPeg.safeGet()
|
||||||
|
.getCrypto()!
|
||||||
|
.createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
||||||
this.setState({
|
this.setState({
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
|
@ -585,6 +610,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
||||||
<div>
|
<div>
|
||||||
<Field
|
<Field
|
||||||
|
id="mx_CreateSecretStorageDialog_password"
|
||||||
type="password"
|
type="password"
|
||||||
label={_t("common|password")}
|
label={_t("common|password")}
|
||||||
value={this.state.accountPassword}
|
value={this.state.accountPassword}
|
||||||
|
@ -595,7 +621,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (!this.state.backupSigStatus?.usable) {
|
} else if (!this.state.backupTrustInfo?.trusted) {
|
||||||
authPrompt = (
|
authPrompt = (
|
||||||
<div>
|
<div>
|
||||||
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
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<MatrixClient>;
|
||||||
|
let mockCrypto: MockedObject<CryptoApi>;
|
||||||
|
|
||||||
|
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(),
|
||||||
|
bootstrapCrossSigning: jest.fn(),
|
||||||
|
bootstrapSecretStorage: jest.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderComponent(
|
||||||
|
props: Partial<React.ComponentProps<typeof CreateSecretStorageDialog>> = {},
|
||||||
|
): RenderResult {
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
return render(<CreateSecretStorageDialog onFinished={onFinished} {...props} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,551 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no key backup is present 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_CreateSecretStorageDialog"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title mx_CreateSecretStorageDialog_centeredTitle"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Set up Secure Backup
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<p
|
||||||
|
class="mx_CreateSecretStorageDialog_centeredBody"
|
||||||
|
>
|
||||||
|
Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mx_CreateSecretStorageDialog_primaryContainer"
|
||||||
|
role="radiogroup"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked mx_StyledRadioButton_outlined"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
name="keyPassphrase"
|
||||||
|
type="radio"
|
||||||
|
value="key"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_StyledRadioButton_content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_CreateSecretStorageDialog_optionTitle"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"
|
||||||
|
/>
|
||||||
|
Generate a Security Key
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_StyledRadioButton_spacer"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_outlined"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="keyPassphrase"
|
||||||
|
type="radio"
|
||||||
|
value="passphrase"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_StyledRadioButton_content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_CreateSecretStorageDialog_optionTitle"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"
|
||||||
|
/>
|
||||||
|
Enter a Security Phrase
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Use a secret phrase only you know, and optionally save a Security Key to use for backup.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_StyledRadioButton_spacer"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-testid="dialog-cancel-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_CreateSecretStorageDialog"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading…"
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CreateSecretStorageDialog when backup is present but not trusted shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_CreateSecretStorageDialog"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Upgrade your encryption
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Restore your key backup to upgrade your encryption
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="danger"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls bootstrapSecretStorage once keys are restored if the backup is now trusted 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_CreateSecretStorageDialog"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Upgrade your encryption
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Enter your account password to confirm the upgrade:
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Field mx_Field_input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="mx_CreateSecretStorageDialog_password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_CreateSecretStorageDialog_password"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="danger"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
disabled=""
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly prompts for a password and then shows RestoreKeyBackupDialog 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_CreateSecretStorageDialog"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Upgrade your encryption
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Enter your account password to confirm the upgrade:
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Field mx_Field_input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="mx_CreateSecretStorageDialog_password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_CreateSecretStorageDialog_password"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="danger"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
disabled=""
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly when there is an error fetching the backup version after RestoreKeyBackupDialog handles the error sensibly 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_CreateSecretStorageDialog"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="mx_Heading_h3 mx_Dialog_title"
|
||||||
|
id="mx_BaseDialog_title"
|
||||||
|
>
|
||||||
|
Upgrade your encryption
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Enter your account password to confirm the upgrade:
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Field mx_Field_input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="mx_CreateSecretStorageDialog_password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_CreateSecretStorageDialog_password"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="danger"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
disabled=""
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`CreateSecretStorageDialog when there is an error fetching the backup version shows an error 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="mx_BaseDialog_title"
|
||||||
|
class="mx_CreateSecretStorageDialog"
|
||||||
|
data-focus-lock-disabled="false"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_header"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Unable to query secret storage status
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_Dialog_buttons_row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-testid="dialog-cancel-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_Dialog_primary"
|
||||||
|
data-testid="dialog-primary-button"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-focus-guard="true"
|
||||||
|
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -65,7 +65,7 @@ export class MockClientWithEventEmitter extends EventEmitter {
|
||||||
});
|
});
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* See also `stubClient()` which does something similar but uses a more complete mock client.
|
* See also {@link stubClient} which does something similar but uses a more complete mock client.
|
||||||
*/
|
*/
|
||||||
export const getMockClientWithEventEmitter = (
|
export const getMockClientWithEventEmitter = (
|
||||||
mockProperties: Partial<Record<keyof MatrixClient, unknown>>,
|
mockProperties: Partial<Record<keyof MatrixClient, unknown>>,
|
||||||
|
@ -152,6 +152,7 @@ export const mockClientMethodsCrypto = (): Partial<
|
||||||
isKeyBackupKeyStored: jest.fn(),
|
isKeyBackupKeyStored: jest.fn(),
|
||||||
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
|
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
|
||||||
getStoredCrossSigningForUser: jest.fn(),
|
getStoredCrossSigningForUser: jest.fn(),
|
||||||
|
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
|
||||||
secretStorage: { hasKey: jest.fn() },
|
secretStorage: { hasKey: jest.fn() },
|
||||||
getCrypto: jest.fn().mockReturnValue({
|
getCrypto: jest.fn().mockReturnValue({
|
||||||
getUserDeviceInfo: jest.fn(),
|
getUserDeviceInfo: jest.fn(),
|
||||||
|
|
|
@ -60,7 +60,7 @@ import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/Matri
|
||||||
* the react context, we can get rid of this and just inject a test client
|
* the react context, we can get rid of this and just inject a test client
|
||||||
* via the context instead.
|
* via the context instead.
|
||||||
*
|
*
|
||||||
* See also `getMockClientWithEventEmitter` which does something similar but different.
|
* See also {@link getMockClientWithEventEmitter} which does something similar but different.
|
||||||
*/
|
*/
|
||||||
export function stubClient(): MatrixClient {
|
export function stubClient(): MatrixClient {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue