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 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");
|
||||
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 FileSaver from "file-saver";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||
import { CrossSigningKeys, IAuthDict, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import classNames from "classnames";
|
||||
import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import { _t, _td } from "../../../../languageHandler";
|
||||
|
@ -81,8 +80,25 @@ interface IState {
|
|||
copied: boolean;
|
||||
downloaded: 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
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: boolean | null;
|
||||
|
@ -144,7 +160,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
downloaded: false,
|
||||
setPassphrase: false,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupTrustInfo: undefined,
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
accountPasswordCorrect: null,
|
||||
|
@ -177,13 +193,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
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 {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
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
|
||||
backupInfo && cli.isCryptoEnabled() ? await cli.isKeyBackupTrusted(backupInfo) : null;
|
||||
backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
||||
|
||||
const { forceReset } = this.props;
|
||||
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
|
||||
|
@ -191,16 +215,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
this.setState({
|
||||
phase,
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
backupTrustInfo,
|
||||
});
|
||||
|
||||
return {
|
||||
backupInfo: backupInfo ?? undefined,
|
||||
backupSigStatus: backupSigStatus ?? undefined,
|
||||
};
|
||||
return backupTrustInfo;
|
||||
} catch (e) {
|
||||
console.error("Error fetching backup data from server", e);
|
||||
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> => {
|
||||
if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
|
||||
this.recoveryKey = await MatrixClientPeg.safeGet().createRecoveryKeyFromPassphrase();
|
||||
this.recoveryKey = await MatrixClientPeg.safeGet().getCrypto()!.createRecoveryKeyFromPassphrase();
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
|
@ -255,7 +277,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
|
||||
private onMigrateFormSubmit = (e: React.FormEvent): void => {
|
||||
e.preventDefault();
|
||||
if (this.state.backupSigStatus?.usable) {
|
||||
if (this.state.backupTrustInfo?.trusted) {
|
||||
this.bootstrapSecretStorage();
|
||||
} else {
|
||||
this.restoreBackup();
|
||||
|
@ -284,7 +306,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
};
|
||||
|
||||
private doBootstrapUIAuth = async (
|
||||
makeRequest: (authData: IAuthDict) => Promise<UIAResponse<void>>,
|
||||
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||
): Promise<void> => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
|
@ -337,13 +359,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
});
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const crypto = cli.getCrypto()!;
|
||||
|
||||
const { forceReset } = this.props;
|
||||
|
||||
try {
|
||||
if (forceReset) {
|
||||
logger.log("Forcing secret storage reset");
|
||||
await cli.bootstrapSecretStorage({
|
||||
await crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this.recoveryKey!,
|
||||
setupNewKeyBackup: true,
|
||||
setupNewSecretStorage: true,
|
||||
|
@ -356,10 +379,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
// * SSO authentication users which require interactive auth to upload
|
||||
// keys (and also happen to skip all post-authentication flows at the
|
||||
// moment via token login)
|
||||
await cli.bootstrapCrossSigning({
|
||||
await crypto.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||
});
|
||||
await cli.bootstrapSecretStorage({
|
||||
await crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => this.recoveryKey!,
|
||||
keyBackupInfo: this.state.backupInfo!,
|
||||
setupNewKeyBackup: !this.state.backupInfo,
|
||||
|
@ -420,8 +443,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
);
|
||||
|
||||
await finished;
|
||||
const { backupSigStatus } = await this.fetchBackupInfo();
|
||||
if (backupSigStatus?.usable && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
const backupTrustInfo = await this.fetchBackupInfo();
|
||||
if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
this.bootstrapSecretStorage();
|
||||
}
|
||||
};
|
||||
|
@ -462,7 +485,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
|
||||
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({
|
||||
copied: 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>
|
||||
<Field
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
type="password"
|
||||
label={_t("common|password")}
|
||||
value={this.state.accountPassword}
|
||||
|
@ -595,7 +621,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (!this.state.backupSigStatus?.usable) {
|
||||
} else if (!this.state.backupTrustInfo?.trusted) {
|
||||
authPrompt = (
|
||||
<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 = (
|
||||
mockProperties: Partial<Record<keyof MatrixClient, unknown>>,
|
||||
|
@ -152,6 +152,7 @@ export const mockClientMethodsCrypto = (): Partial<
|
|||
isKeyBackupKeyStored: jest.fn(),
|
||||
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
|
||||
getStoredCrossSigningForUser: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
|
||||
secretStorage: { hasKey: jest.fn() },
|
||||
getCrypto: jest.fn().mockReturnValue({
|
||||
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
|
||||
* 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 {
|
||||
const client = createTestClient();
|
||||
|
|
Loading…
Reference in a new issue