From 0bbed853daec9acdfb4b95fa78c0796f614d3124 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Sep 2024 16:36:30 +0100 Subject: [PATCH 01/21] Pass bot token through explicitly (#56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because apparently secrets: inherit only works for environment secrets, and it only took me several hours of research to track this down 🙄 --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eddca83555..237a549738 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,8 @@ concurrency: ${{ github.workflow }} jobs: release: uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop - secrets: inherit + secrets: + ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} with: final: ${{ inputs.mode == 'final' }} npm: ${{ inputs.npm }} From 154bf33fa1284c83c4abb52e9cdaab726515e603 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 18 Sep 2024 17:56:18 +0100 Subject: [PATCH 02/21] Manually clear orphaned recaptcha challenge overlay in android webviews (#53) * try manually clear any left over recaptcha overalys * add alert to debug webview * disable settings check for netlify build * Update fix for removing challenge, as g-recaptcha-bubble-arrow is now always shown * Remove alert for debugging webview * Put back requirement for config setting and make sure it redirects to welcome if not present. * Add comment to explain bodge. * Remove unrelated code --- src/components/views/auth/CaptchaForm.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index 2b82f4cea0..f216f004fd 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -63,6 +63,19 @@ export default class CaptchaForm extends React.Component Date: Thu, 19 Sep 2024 08:13:04 +0100 Subject: [PATCH 03/21] Playwright: factor out some common code (#49) * playwright: factor out `bootstrapCrossSigningForClient` method Pull this out so it can be used elsewhere. Also expose the `resetKeys` param, which might be useful in future. * playwright: bot.ts: use `bootstrapCrossSigningForClient` ... instead of reinventing it. * Only setup cross signing if `startClient` is set --- playwright/pages/bot.ts | 89 +++++++++++++++++++------------------- playwright/pages/client.ts | 45 ++++++++++++------- 2 files changed, 75 insertions(+), 59 deletions(-) diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index b7542338b6..d50a0e84ee 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -14,7 +14,7 @@ import type { Logger } from "matrix-js-sdk/src/logger"; import type { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; import type { Credentials, HomeserverInstance } from "../plugins/homeserver"; import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; -import { Client } from "./client"; +import { bootstrapCrossSigningForClient, Client } from "./client"; export interface CreateBotOpts { /** @@ -90,9 +90,13 @@ export class Bot extends Client { } protected async getClientHandle(): Promise> { - if (this.handlePromise) return this.handlePromise; + if (!this.handlePromise) this.handlePromise = this.buildClient(); + return this.handlePromise; + } - this.handlePromise = this.page.evaluateHandle( + private async buildClient(): Promise> { + const credentials = await this.getCredentials(); + const clientHandle = await this.page.evaluateHandle( async ({ homeserver, credentials, opts }) => { function getLogger(loggerName: string): Logger { const logger = { @@ -172,53 +176,50 @@ export class Bot extends Client { }); } - if (!opts.startClient) { - return cli; - } - - await cli.initRustCrypto({ useIndexedDB: false }); - cli.setGlobalErrorOnUnknownDevices(false); - await cli.startClient(); - - if (opts.bootstrapCrossSigning) { - // XXX: workaround https://github.com/element-hq/element-web/issues/26755 - // wait for out device list to be available, as a proxy for the device keys having been uploaded. - await cli.getCrypto()!.getUserDeviceInfo([credentials.userId]); - - await cli.getCrypto()!.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (func) => { - await func({ - type: "m.login.password", - identifier: { - type: "m.id.user", - user: credentials.userId, - }, - password: credentials.password, - }); - }, - }); - } - - if (opts.bootstrapSecretStorage) { - const passphrase = "new passphrase"; - const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase); - Object.assign(cli, { __playwright_recovery_key: recoveryKey }); - - await cli.getCrypto()!.bootstrapSecretStorage({ - setupNewSecretStorage: true, - setupNewKeyBackup: true, - createSecretStorageKey: () => Promise.resolve(recoveryKey), - }); - } - return cli; }, { homeserver: this.homeserver.config, - credentials: await this.getCredentials(), + credentials, opts: this.opts, }, ); - return this.handlePromise; + + // If we weren't configured to start the client, bail out now. + if (!this.opts.startClient) { + return clientHandle; + } + + await clientHandle.evaluate(async (cli) => { + await cli.initRustCrypto({ useIndexedDB: false }); + cli.setGlobalErrorOnUnknownDevices(false); + await cli.startClient(); + }); + + if (this.opts.bootstrapCrossSigning) { + // XXX: workaround https://github.com/element-hq/element-web/issues/26755 + // wait for out device list to be available, as a proxy for the device keys having been uploaded. + await clientHandle.evaluate(async (cli, credentials) => { + await cli.getCrypto()!.getUserDeviceInfo([credentials.userId]); + }, credentials); + + await bootstrapCrossSigningForClient(clientHandle, credentials); + } + + if (this.opts.bootstrapSecretStorage) { + await clientHandle.evaluate(async (cli) => { + const passphrase = "new passphrase"; + const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase); + Object.assign(cli, { __playwright_recovery_key: recoveryKey }); + + await cli.getCrypto()!.bootstrapSecretStorage({ + setupNewSecretStorage: true, + setupNewKeyBackup: true, + createSecretStorageKey: () => Promise.resolve(recoveryKey), + }); + }); + } + + return clientHandle; } } diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts index 002a3340b2..06e05fdcfa 100644 --- a/playwright/pages/client.ts +++ b/playwright/pages/client.ts @@ -356,24 +356,11 @@ export class Client { } /** - * Boostraps cross-signing. + * Bootstraps cross-signing. */ public async bootstrapCrossSigning(credentials: Credentials): Promise { const client = await this.prepareClient(); - return client.evaluate(async (client, credentials) => { - await client.getCrypto().bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async (func) => { - await func({ - type: "m.login.password", - identifier: { - type: "m.id.user", - user: credentials.userId, - }, - password: credentials.password, - }); - }, - }); - }, credentials); + return bootstrapCrossSigningForClient(client, credentials); } /** @@ -439,3 +426,31 @@ export class Client { ); } } + +/** Call `CryptoApi.bootstrapCrossSigning` on the given Matrix client, using the given credentials to authenticate + * the UIA request. + */ +export function bootstrapCrossSigningForClient( + client: JSHandle, + credentials: Credentials, + resetKeys: boolean = false, +) { + return client.evaluate( + async (client, { credentials, resetKeys }) => { + await client.getCrypto().bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async (func) => { + await func({ + type: "m.login.password", + identifier: { + type: "m.id.user", + user: credentials.userId, + }, + password: credentials.password, + }); + }, + setupNewCrossSigning: resetKeys, + }); + }, + { credentials, resetKeys }, + ); +} From 3dd223c7dde423ab5eeee2c773411c0172977cfc Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Sep 2024 09:37:25 +0100 Subject: [PATCH 04/21] Also add NPM_TOKEN (#57) --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 237a549738..9d1cb6e2d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,7 @@ jobs: uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop secrets: ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} with: final: ${{ inputs.mode == 'final' }} npm: ${{ inputs.npm }} From 0cc0ebe2edeb5491a725b2f97d125eaff1b17792 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 19 Sep 2024 11:34:04 +0200 Subject: [PATCH 05/21] Replace old reference of `matrix-org/matrix-react-sdk` by `element-hq/matrix-react-sdk` (#60) --- .github/workflows/notify-element-web.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index 522a089edb..442a2f1082 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -9,7 +9,7 @@ jobs: name: "Notify Element Web" runs-on: ubuntu-latest # Only respect triggers from our develop branch, ignore that of forks - if: github.repository == 'matrix-org/matrix-react-sdk' + if: github.repository == 'element-hq/matrix-react-sdk' steps: - name: Notify element-web repo that a new SDK build is on develop uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8418a9519..e306b32959 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,7 +39,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - repository: ${{ inputs.matrix-js-sdk-sha && 'matrix-org/matrix-react-sdk' || github.repository }} + repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/matrix-react-sdk' || github.repository }} - name: Yarn cache uses: actions/setup-node@v4 @@ -111,7 +111,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - repository: ${{ inputs.matrix-js-sdk-sha && 'matrix-org/matrix-react-sdk' || github.repository }} + repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/matrix-react-sdk' || github.repository }} - uses: actions/setup-node@v4 with: From 4776f87775f936a4b1319ee6b04b486fcf1133ef Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 19 Sep 2024 11:39:32 +0200 Subject: [PATCH 06/21] Ignore chat effect when older than 48h (#48) * Ignore effect later than 48h * Add tests for `EffectsOverlay-test.tsx` --- src/components/structures/RoomView.tsx | 2 +- .../views/elements/EffectsOverlay.tsx | 22 +++++++- .../views/elements/EffectsOverlay-test.tsx | 51 +++++++++++++++++++ .../EffectsOverlay-test.tsx.snap | 12 +++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 test/components/views/elements/EffectsOverlay-test.tsx create mode 100644 test/components/views/elements/__snapshots__/EffectsOverlay-test.tsx.snap diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a65743a3e2..87e8c3c307 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1364,7 +1364,7 @@ export class RoomView extends React.Component { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { // For initial threads launch, chat effects are disabled see #19731 if (!ev.isRelation(THREAD_RELATION_TYPE.name)) { - dis.dispatch({ action: `effects.${effect.command}` }); + dis.dispatch({ action: `effects.${effect.command}`, event: ev }); } } }); diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 76262ad4f0..3e5a5ead60 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { FunctionComponent, useEffect, useRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import dis from "../../../dispatcher/dispatcher"; import ICanvasEffect from "../../../effects/ICanvasEffect"; @@ -44,9 +45,10 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { canvasRef.current.height = UIStore.instance.windowHeight; } }; - const onAction = (payload: { action: string }): void => { + const onAction = (payload: { action: string; event?: MatrixEvent }): void => { const actionPrefix = "effects."; - if (canvasRef.current && payload.action.startsWith(actionPrefix)) { + const isOutdated = isEventOutdated(payload.event); + if (canvasRef.current && payload.action.startsWith(actionPrefix) && !isOutdated) { const effect = payload.action.slice(actionPrefix.length); lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current!)); } @@ -88,3 +90,19 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { }; export default EffectsOverlay; + +// 48 hours +// 48h * 60m * 60s * 1000ms +const OUTDATED_EVENT_THRESHOLD = 48 * 60 * 60 * 1000; + +/** + * Return true if the event is older than 48h. + * @param event + */ +function isEventOutdated(event?: MatrixEvent): boolean { + if (!event) return false; + + const nowTs = Date.now(); + const eventTs = event.getTs(); + return nowTs - eventTs > OUTDATED_EVENT_THRESHOLD; +} diff --git a/test/components/views/elements/EffectsOverlay-test.tsx b/test/components/views/elements/EffectsOverlay-test.tsx new file mode 100644 index 0000000000..48e508181d --- /dev/null +++ b/test/components/views/elements/EffectsOverlay-test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + * + */ + +import React from "react"; +import { render, waitFor } from "@testing-library/react"; + +import dis from "../../../../src/dispatcher/dispatcher"; +import EffectsOverlay from "../../../../src/components/views/elements/EffectsOverlay.tsx"; + +describe("", () => { + let isStarted: boolean; + beforeEach(() => { + isStarted = false; + jest.mock("../../../../src/effects/confetti/index.ts", () => { + return class Confetti { + start = () => { + isStarted = true; + }; + stop = jest.fn(); + }; + }); + }); + + afterEach(() => jest.useRealTimers()); + + it("should render", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should start the confetti effect", async () => { + render(); + dis.dispatch({ action: "effects.confetti" }); + await waitFor(() => expect(isStarted).toBe(true)); + }); + + it("should start the confetti effect when the event is not outdated", async () => { + const eventDate = new Date("2024-09-01"); + const date = new Date("2024-09-02"); + jest.useFakeTimers().setSystemTime(date); + + render(); + dis.dispatch({ action: "effects.confetti", event: { getTs: () => eventDate.getTime() } }); + await waitFor(() => expect(isStarted).toBe(true)); + }); +}); diff --git a/test/components/views/elements/__snapshots__/EffectsOverlay-test.tsx.snap b/test/components/views/elements/__snapshots__/EffectsOverlay-test.tsx.snap new file mode 100644 index 0000000000..222d893e99 --- /dev/null +++ b/test/components/views/elements/__snapshots__/EffectsOverlay-test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = ` + + +`; From 490746e56aab4227f64895f43de24d2b189c0ffe Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 19 Sep 2024 15:41:11 +0200 Subject: [PATCH 07/21] Update to use non deprecated methods to derive key from passphrase (#55) * Replace `deriveKey` call by `deriveRecoveryKeyFromPassphrase` * Remove `matrix-js-sdk/src/crypto/key_passphrase` import of eslint exception --- .eslintrc.js | 1 - src/SecurityManager.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 444388d492..9cb3e29fb7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -122,7 +122,6 @@ module.exports = { "!matrix-js-sdk/src/crypto/aes", "!matrix-js-sdk/src/crypto/keybackup", "!matrix-js-sdk/src/crypto/deviceinfo", - "!matrix-js-sdk/src/crypto/key_passphrase", "!matrix-js-sdk/src/crypto/recoverykey", "!matrix-js-sdk/src/crypto/dehydration", "!matrix-js-sdk/src/oidc", diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 6bfd361140..f03f83d573 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix"; -import { deriveKey } from "matrix-js-sdk/src/crypto/key_passphrase"; +import { deriveRecoveryKeyFromPassphrase } from "matrix-js-sdk/src/crypto-api"; import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey"; import { logger } from "matrix-js-sdk/src/logger"; @@ -64,7 +64,7 @@ function makeInputToKey( ): (keyParams: KeyParams) => Promise { return async ({ passphrase, recoveryKey }): Promise => { if (passphrase) { - return deriveKey(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations); + return deriveRecoveryKeyFromPassphrase(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations); } else if (recoveryKey) { return decodeRecoveryKey(recoveryKey); } From fe657027bdaf54c3d7e62aee8a0fe50fce53dee5 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 19 Sep 2024 17:39:20 +0200 Subject: [PATCH 08/21] Update to use non deprecated methods to decode recovery key (#54) * Replace `MatrixClient.keyBackupKeyFromRecoveryKey` by `decodeRecoveryKey` * Replace `MatrixClient.isValidRecoveryKey` by local check with `decodeRecoveryKey` * Replace old `decodeRecoveryKey` import * Remove `matrix-js-sdk/src/crypto/recoverykey` import of eslint exception * Add tests for `RestoreKeyBackupDialog` --- .eslintrc.js | 1 - src/SecurityManager.ts | 3 +- .../security/AccessSecretStorageDialog.tsx | 3 +- .../security/RestoreKeyBackupDialog.tsx | 20 +- .../AccessSecretStorageDialog-test.tsx | 8 +- .../security/RestoreKeyBackupDialog-test.tsx | 51 +++ .../RestoreKeyBackupDialog-test.tsx.snap | 298 ++++++++++++++++++ 7 files changed, 371 insertions(+), 13 deletions(-) create mode 100644 test/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx create mode 100644 test/components/views/dialogs/security/__snapshots__/RestoreKeyBackupDialog-test.tsx.snap diff --git a/.eslintrc.js b/.eslintrc.js index 9cb3e29fb7..a3c7eb4f8d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -122,7 +122,6 @@ module.exports = { "!matrix-js-sdk/src/crypto/aes", "!matrix-js-sdk/src/crypto/keybackup", "!matrix-js-sdk/src/crypto/deviceinfo", - "!matrix-js-sdk/src/crypto/recoverykey", "!matrix-js-sdk/src/crypto/dehydration", "!matrix-js-sdk/src/oidc", "!matrix-js-sdk/src/oidc/discovery", diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index f03f83d573..fb73e25389 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix"; -import { deriveRecoveryKeyFromPassphrase } from "matrix-js-sdk/src/crypto-api"; -import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey"; +import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog"; diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 3759e11063..0c4e875607 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -10,6 +10,7 @@ import { debounce } from "lodash"; import classNames from "classnames"; import React, { ChangeEvent, FormEvent } from "react"; import { logger } from "matrix-js-sdk/src/logger"; +import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; import { SecretStorage } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; @@ -100,7 +101,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent => {}, /* forceReset = */ true); }; + /** + * Check if the recovery key is valid + * @param recoveryKey + * @private + */ + private isValidRecoveryKey(recoveryKey: string): boolean { + try { + decodeRecoveryKey(recoveryKey); + return true; + } catch (e) { + return false; + } + } + private onRecoveryKeyChange = (e: ChangeEvent): void => { this.setState({ recoveryKey: e.target.value, - recoveryKeyValid: MatrixClientPeg.safeGet().isValidRecoveryKey(e.target.value), + recoveryKeyValid: this.isValidRecoveryKey(e.target.value), }); }; @@ -184,7 +198,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { beforeEach(() => { mockClient = getMockClientWithEventEmitter({ - keyBackupKeyFromRecoveryKey: jest.fn(), checkSecretStorageKey: jest.fn(), - isValidRecoveryKey: jest.fn(), }); }); it("Closes the dialog when the form is submitted with a valid key", async () => { mockClient.checkSecretStorageKey.mockResolvedValue(true); - mockClient.isValidRecoveryKey.mockReturnValue(true); const onFinished = jest.fn(); const checkPrivateKey = jest.fn().mockResolvedValue(true); @@ -88,8 +85,8 @@ describe("AccessSecretStorageDialog", () => { const checkPrivateKey = jest.fn().mockResolvedValue(true); renderComponent({ onFinished, checkPrivateKey }); - mockClient.keyBackupKeyFromRecoveryKey.mockImplementation(() => { - throw new Error("that's no key"); + mockClient.checkSecretStorageKey.mockImplementation(() => { + throw new Error("invalid key"); }); await enterSecurityKey(); @@ -115,7 +112,6 @@ describe("AccessSecretStorageDialog", () => { }; const checkPrivateKey = jest.fn().mockResolvedValue(false); renderComponent({ checkPrivateKey, keyInfo }); - mockClient.isValidRecoveryKey.mockReturnValue(false); await enterSecurityKey("Security Phrase"); expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey); diff --git a/test/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx b/test/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx new file mode 100644 index 0000000000..3e52b473b6 --- /dev/null +++ b/test/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + * + */ + +import React from "react"; +import { screen, render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +// Needed to be able to mock decodeRecoveryKey +// eslint-disable-next-line no-restricted-imports +import * as recoveryKeyModule from "matrix-js-sdk/src/crypto-api/recovery-key"; + +import RestoreKeyBackupDialog from "../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx"; +import { stubClient } from "../../../../test-utils"; + +describe("", () => { + beforeEach(() => { + stubClient(); + jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32)); + }); + + it("should render", async () => { + const { asFragment } = render(); + await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument()); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should display an error when recovery key is invalid", async () => { + jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockImplementation(() => { + throw new Error("Invalid recovery key"); + }); + const { asFragment } = render(); + await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument()); + + await userEvent.type(screen.getByRole("textbox"), "invalid key"); + await waitFor(() => expect(screen.getByText("👎 Not a valid Security Key")).toBeInTheDocument()); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should not raise an error when recovery is valid", async () => { + const { asFragment } = render(); + await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument()); + + await userEvent.type(screen.getByRole("textbox"), "valid key"); + await waitFor(() => expect(screen.getByText("👍 This looks like a valid Security Key!")).toBeInTheDocument()); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/dialogs/security/__snapshots__/RestoreKeyBackupDialog-test.tsx.snap b/test/components/views/dialogs/security/__snapshots__/RestoreKeyBackupDialog-test.tsx.snap new file mode 100644 index 0000000000..de0bddbe33 --- /dev/null +++ b/test/components/views/dialogs/security/__snapshots__/RestoreKeyBackupDialog-test.tsx.snap @@ -0,0 +1,298 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should display an error when recovery key is invalid 1`] = ` + +
+