diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml new file mode 100644 index 00000000000..978b27bae4b --- /dev/null +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -0,0 +1,33 @@ +name: Upload release assets +description: Uploads assets to an existing release and optionally signs them +inputs: + tag: + description: GitHub release tag to fetch assets from. + required: true + out-file-path: + description: Path to where the webapp should be extracted to. + required: true +runs: + using: composite + steps: + - name: Download current version for its old bundles + id: current_download + uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 + with: + tag: steps.current_version.outputs.version + fileName: element-*.tar.gz* + out-file-path: ${{ runner.temp }}/download-verify-element-tarball + + - name: Verify tarball + run: gpg --verify element-*.tar.gz.asc element-*.tar.gz + working-directory: ${{ runner.temp }}/download-verify-element-tarball + + - name: Extract tarball + run: tar xvzf element-*.tar.gz -C webapp --strip-components=1 + working-directory: ${{ runner.temp }}/download-verify-element-tarball + + - name: Move webapp to out-file-path + run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }} + + - name: Clean up temp directory + run: rm -R ${{ runner.temp }}/download-verify-element-tarball diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml index c21ab831e6a..8bbcfe726f5 100644 --- a/.github/workflows/build_develop.yml +++ b/.github/workflows/build_develop.yml @@ -20,6 +20,7 @@ jobs: permissions: checks: read pages: write + deployments: write env: R2_BUCKET: "element-web-develop" R2_URL: ${{ vars.CF_R2_S3_API }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000000..a41a4dcec74 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,88 @@ +# Manual deploy workflow for deploying to app.element.io & staging.element.io +# Runs automatically for staging.element.io when an RC or Release is published +# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand +name: Build and Deploy ${{ inputs.site || 'staging.element.io' }} +on: + release: + types: [published] + workflow_dispatch: + inputs: + site: + description: Which site to deploy to + required: true + default: staging.element.io + type: choice + options: + - staging.element.io + - app.element.io +concurrency: ${{ inputs.site || 'staging.element.io' }} +permissions: {} +jobs: + deploy: + name: "Deploy to Cloudflare Pages" + runs-on: ubuntu-24.04 + environment: ${{ inputs.site || 'staging.element.io' }} + permissions: + checks: read + deployments: write + env: + SITE: ${{ inputs.site || 'staging.element.io' }} + steps: + - name: Load GPG key + run: | + curl https://packages.element.io/element-release-key.gpg | gpg --import + gpg -k "$GPG_FINGERPRINT" + env: + GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }} + + - name: Check current version on deployment + id: current_version + run: | + echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT + + # The current version bundle melding dance is skipped if the version we're deploying is the same + # as then we're just doing a re-deploy of the same version with potentially different configs. + - name: Download current version for its old bundles + id: current_download + if: steps.current_version.outputs.version != github.ref_name + uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} + with: + tag: steps.current_version.outputs.version + out-file-path: current_version + + - name: Download target version + uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} + with: + tag: ${{ github.ref_name }} + out-file-path: _deploy + + - name: Merge current bundles into target + if: steps.current_download.outcome == 'success' + run: cp -vnpr current_version/bundles/* _deploy/bundles/ + + - name: Copy config + run: cp element.io/app/config.json _deploy/config.json + + - name: Populate 404.html + run: echo "404 Not Found" > _deploy/404.html + + - name: Populate _headers + run: cp .github/cfp_headers _deploy/_headers + + - name: Wait for other steps to succeed + uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork + with: + ref: ${{ github.sha }} + running-workflow-name: "Build and Deploy ${{ env.SITE }}" + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 10 + check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$ + + - name: Deploy to Cloudflare Pages + uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1 + with: + apiToken: ${{ secrets.CF_PAGES_TOKEN }} + accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }} + projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }} + directory: _deploy + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 0a14950b1f4..7ccbd7def30 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:127c68d4468019ce363c8b2fd7a42a3ef50710eb3aaf288a2295dd4623ce9f54"; +const DOCKER_TAG = "develop@sha256:34da08a44994e0ad2def7ed5f28c3cc7a2f7ead9722f4ae87b23030f59384ea5"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index dcef9c2eb93..73366f2fee3 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -22,6 +22,13 @@ declare module "matrix-js-sdk/src/types" { [BLURHASH_FIELD]?: string; } + export interface ImageInfo { + /** + * @see https://github.com/matrix-org/matrix-spec-proposals/pull/4230 + */ + "org.matrix.msc4230.is_animated"?: boolean; + } + export interface StateEvents { // Jitsi-backed video room state events [JitsiCallMemberEventType]: JitsiCallMemberContent; diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 895e168f3b8..344a2f112c3 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -56,6 +56,7 @@ import { createThumbnail } from "./utils/image-media"; import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer"; import { doMaybeLocalRoomAction } from "./utils/local-room"; import { SdkContextClass } from "./contexts/SDKContext"; +import { blobIsAnimated } from "./utils/Image.ts"; // scraped out of a macOS hidpi (5660ppm) screenshot png // 5669 px (x-axis) , 5669 px (y-axis) , per metre @@ -150,15 +151,20 @@ async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imag thumbnailType = "image/jpeg"; } + // We don't await this immediately so it can happen in the background + const isAnimatedPromise = blobIsAnimated(imageFile.type, imageFile); + const imageElement = await loadImageElement(imageFile); const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType); const imageInfo = result.info; + imageInfo["org.matrix.msc4230.is_animated"] = await isAnimatedPromise; + // For lesser supported image types, always include the thumbnail even if it is larger if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) { // we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from. - const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size; + const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size!; if ( // image is small enough already imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 4f47cd7eac4..84d83827da5 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -230,12 +230,15 @@ export default class DeviceListener { private async getKeyBackupInfo(): Promise { if (!this.client) return null; const now = new Date().getTime(); + const crypto = this.client.getCrypto(); + if (!crypto) return null; + if ( !this.keyBackupInfo || !this.keyBackupFetchedAt || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL ) { - this.keyBackupInfo = await this.client.getKeyBackupVersion(); + this.keyBackupInfo = await crypto.getKeyBackupInfo(); this.keyBackupFetchedAt = now; } return this.keyBackupInfo; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 1258bde2cad..932f6d7fcf8 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -279,7 +279,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } else { // otherwise check the server to see if there's a new one try { - newVersionInfo = await cli.getKeyBackupVersion(); + newVersionInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null; if (newVersionInfo !== null) haveNewVersion = true; } catch (e) { logger.error("Saw key backup error but failed to check backup version!", e); diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index cdfaf0e89b1..4731c593bc3 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -109,7 +109,7 @@ export default class LogoutDialog extends React.Component { } // backup is not active. see if there is a backup version on the server we ought to back up to. - const backupInfo = await client.getKeyBackupVersion(); + const backupInfo = await crypto.getKeyBackupInfo(); this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP }); } diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index ec85e72ac9d..fddba10948b 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -258,7 +258,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { } const content = this.props.mxEvent.getContent(); - let isAnimated = mayBeAnimated(content.info?.mimetype); + let isAnimated = content.info?.["org.matrix.msc4230.is_animated"] ?? mayBeAnimated(content.info?.mimetype); // If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server // because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail. @@ -298,8 +298,15 @@ export default class MImageBody extends React.Component { } try { - const blob = await this.props.mediaEventHelper!.sourceBlob.value; - if (!(await blobIsAnimated(content.info?.mimetype, blob))) { + // If we didn't receive the MSC4230 is_animated flag + // then we need to check if the image is animated by downloading it. + if ( + content.info?.["org.matrix.msc4230.is_animated"] === false || + !(await blobIsAnimated( + content.info?.mimetype, + await this.props.mediaEventHelper!.sourceBlob.value, + )) + ) { isAnimated = false; } diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 06c67c7d0b3..05c78296441 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -118,7 +118,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { this.getUpdatedDiagnostics(); try { const cli = MatrixClientPeg.safeGet(); - const backupInfo = await cli.getKeyBackupVersion(); + const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null; const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined; const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null; @@ -192,12 +192,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { if (!proceed) return; this.setState({ loading: true }); const versionToDelete = this.state.backupInfo!.version!; - MatrixClientPeg.safeGet() - .getCrypto() - ?.deleteKeyBackupVersion(versionToDelete) - .then(() => { - this.loadBackupStatus(); - }); + // deleteKeyBackupVersion fires a key backup status event + // which will update the UI + MatrixClientPeg.safeGet().getCrypto()?.deleteKeyBackupVersion(versionToDelete); }, }); }; diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 2fb9c6a9ca7..70c721b1ca3 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -125,7 +125,7 @@ export class SetupEncryptionStore extends EventEmitter { this.emit("update"); try { const cli = MatrixClientPeg.safeGet(); - const backupInfo = await cli.getKeyBackupVersion(); + const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null; this.backupInfo = backupInfo; this.emit("update"); diff --git a/src/utils/image-media.ts b/src/utils/image-media.ts index 5e0fb076785..5c013c7b1ac 100644 --- a/src/utils/image-media.ts +++ b/src/utils/image-media.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { EncryptedFile } from "matrix-js-sdk/src/types"; +import { ImageInfo } from "matrix-js-sdk/src/types"; import { BlurhashEncoder } from "../BlurhashEncoder"; @@ -15,19 +15,7 @@ type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 interface IThumbnail { - info: { - thumbnail_info?: { - w: number; - h: number; - mimetype: string; - size: number; - }; - w: number; - h: number; - [BLURHASH_FIELD]?: string; - thumbnail_url?: string; - thumbnail_file?: EncryptedFile; - }; + info: ImageInfo; thumbnail: Blob; } diff --git a/src/vector/platform/ElectronPlatform.tsx b/src/vector/platform/ElectronPlatform.tsx index d7ebd94bb21..ac6e7a7feb1 100644 --- a/src/vector/platform/ElectronPlatform.tsx +++ b/src/vector/platform/ElectronPlatform.tsx @@ -474,10 +474,8 @@ export default class ElectronPlatform extends BasePlatform { const url = super.getOidcCallbackUrl(); url.protocol = "io.element.desktop"; // Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1 - // Chrome seems to have a strange issue where non-standard protocols prevent URL object mutations on pathname - // field, so we cannot mutate `pathname` reliably and instead have to rewrite the href manually. - if (url.pathname.startsWith("//")) { - url.href = url.href.replace(url.pathname, url.pathname.slice(1)); + if (url.href.startsWith(`${url.protocol}://`)) { + url.href = url.href.replace("://", ":/"); } return url; } diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 7842afbfe51..a2347f90588 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -143,7 +143,6 @@ export const mockClientMethodsCrypto = (): Partial< > => ({ isKeyBackupKeyStored: jest.fn(), getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), - getKeyBackupVersion: jest.fn().mockResolvedValue(null), secretStorage: { hasKey: jest.fn() }, getCrypto: jest.fn().mockReturnValue({ getUserDeviceInfo: jest.fn(), @@ -163,6 +162,7 @@ export const mockClientMethodsCrypto = (): Partial< getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})), getCrossSigningKeyId: jest.fn(), isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), }), }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index e5fff8caf73..6eae737dffa 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -99,7 +99,6 @@ export function createTestClient(): MatrixClient { getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }), getSessionId: jest.fn().mockReturnValue("iaszphgvfku"), credentials: { userId: "@userId:matrix.org" }, - getKeyBackupVersion: jest.fn(), secretStorage: { get: jest.fn(), @@ -135,6 +134,7 @@ export function createTestClient(): MatrixClient { restoreKeyBackupWithPassphrase: jest.fn(), loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(), storeSessionBackupPrivateKey: jest.fn(), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), }), getPushActionsForEvent: jest.fn(), diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 906826e4564..ad7f14e1190 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -96,12 +96,12 @@ describe("DeviceListener", () => { }), getSessionBackupPrivateKey: jest.fn(), isEncryptionEnabledInRoom: jest.fn(), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), } as unknown as Mocked; mockClient = getMockClientWithEventEmitter({ isGuest: jest.fn(), getUserId: jest.fn().mockReturnValue(userId), getSafeUserId: jest.fn().mockReturnValue(userId), - getKeyBackupVersion: jest.fn().mockResolvedValue(undefined), getRooms: jest.fn().mockReturnValue([]), isVersionSupported: jest.fn().mockResolvedValue(true), isInitialSyncComplete: jest.fn().mockReturnValue(true), @@ -354,7 +354,7 @@ describe("DeviceListener", () => { it("shows set up encryption toast when user has a key backup available", async () => { // non falsy response - mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo); + mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( @@ -673,7 +673,7 @@ describe("DeviceListener", () => { describe("When Room Key Backup is not enabled", () => { beforeEach(() => { // no backup - mockClient.getKeyBackupVersion.mockResolvedValue(null); + mockCrypto.getKeyBackupInfo.mockResolvedValue(null); }); it("Should report recovery state as Enabled", async () => { @@ -722,7 +722,7 @@ describe("DeviceListener", () => { }); // no backup - mockClient.getKeyBackupVersion.mockResolvedValue(null); + mockCrypto.getKeyBackupInfo.mockResolvedValue(null); await createAndStart(); @@ -872,7 +872,7 @@ describe("DeviceListener", () => { describe("When Room Key Backup is enabled", () => { beforeEach(() => { // backup enabled - just need a mock object - mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); + mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); }); const testCases = [ diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 5cee50ef29d..28bf99fa978 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -139,6 +139,7 @@ describe("", () => { globalBlacklistUnverifiedDevices: false, // This needs to not finish immediately because we need to test the screen appears bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), }), secretStorage: { isStored: jest.fn().mockReturnValue(null), @@ -148,7 +149,6 @@ describe("", () => { whoami: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), - getKeyBackupVersion: jest.fn().mockResolvedValue(null), }); let mockClient: Mocked; const serverConfig = { diff --git a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx index 46fe519b471..0557e538d0e 100644 --- a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx @@ -22,7 +22,6 @@ describe("LogoutDialog", () => { beforeEach(() => { mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsCrypto(), - getKeyBackupVersion: jest.fn(), }); mockCrypto = mocked(mockClient.getCrypto()!); @@ -50,14 +49,14 @@ describe("LogoutDialog", () => { }); it("Prompts user to connect backup if there is a backup on the server", async () => { - mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); + mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); const rendered = renderComponent(); await rendered.findByText("Connect this session to Key Backup"); expect(rendered.container).toMatchSnapshot(); }); it("Prompts user to set up backup if there is no backup on the server", async () => { - mockClient.getKeyBackupVersion.mockResolvedValue(null); + mockCrypto.getKeyBackupInfo.mockResolvedValue(null); const rendered = renderComponent(); await rendered.findByText("Start using Key Backup"); expect(rendered.container).toMatchSnapshot(); @@ -69,7 +68,7 @@ describe("LogoutDialog", () => { describe("when there is an error fetching backups", () => { filterConsole("Unable to fetch key backup status"); it("prompts user to set up backup", async () => { - mockClient.getKeyBackupVersion.mockImplementation(async () => { + mockCrypto.getKeyBackupInfo.mockImplementation(async () => { throw new Error("beep"); }); const rendered = renderComponent(); diff --git a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx index 9e792a48f30..814fe8a9543 100644 --- a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx @@ -77,7 +77,7 @@ describe("CreateSecretStorageDialog", () => { filterConsole("Error fetching backup data from server"); it("shows an error", async () => { - mockClient.getKeyBackupVersion.mockImplementation(async () => { + jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => { throw new Error("bleh bleh"); }); @@ -92,7 +92,7 @@ describe("CreateSecretStorageDialog", () => { expect(result.container).toMatchSnapshot(); // Now we can get the backup and we retry - mockClient.getKeyBackupVersion.mockRestore(); + jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockRestore(); await userEvent.click(screen.getByRole("button", { name: "Retry" })); await screen.findByText("Your keys are now being backed up from this device."); }); diff --git a/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx index 4cfa74073b5..c19010a0893 100644 --- a/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx @@ -28,7 +28,7 @@ describe("", () => { beforeEach(() => { matrixClient = stubClient(); jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32)); - jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ version: "1" } as KeyBackupInfo); + jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1" } as KeyBackupInfo); }); it("should render", async () => { @@ -99,7 +99,7 @@ describe("", () => { test("should restore key backup when passphrase is filled", async () => { // Determine that the passphrase is required - jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ + jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1", auth_data: { private_key_salt: "salt", diff --git a/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx b/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx index f2aa15f3556..63490bf9150 100644 --- a/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx +++ b/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx @@ -28,14 +28,13 @@ describe("", () => { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), ...mockClientMethodsCrypto(), - getKeyBackupVersion: jest.fn().mockReturnValue("1"), getClientWellKnown: jest.fn(), }); const getComponent = () => render(); beforeEach(() => { - client.getKeyBackupVersion.mockResolvedValue({ + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1", algorithm: "test", auth_data: { @@ -52,7 +51,6 @@ describe("", () => { }); mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false); - client.getKeyBackupVersion.mockClear(); mocked(accessSecretStorage).mockClear().mockResolvedValue(); }); @@ -65,8 +63,8 @@ describe("", () => { }); it("handles error fetching backup", async () => { - // getKeyBackupVersion can fail for various reasons - client.getKeyBackupVersion.mockImplementation(async () => { + // getKeyBackupInfo can fail for various reasons + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => { throw new Error("beep beep"); }); const renderResult = getComponent(); @@ -75,9 +73,9 @@ describe("", () => { }); it("handles absence of backup", async () => { - client.getKeyBackupVersion.mockResolvedValue(null); + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null); getComponent(); - // flush getKeyBackupVersion promise + // flush getKeyBackupInfo promise await flushPromises(); expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument(); }); @@ -120,7 +118,7 @@ describe("", () => { }); it("deletes backup after confirmation", async () => { - client.getKeyBackupVersion + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo") .mockResolvedValueOnce({ version: "1", algorithm: "test", @@ -157,7 +155,7 @@ describe("", () => { // flush checkKeyBackup promise await flushPromises(); - client.getKeyBackupVersion.mockClear(); + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear(); mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear(); fireEvent.click(screen.getByText("Reset")); @@ -167,7 +165,7 @@ describe("", () => { await flushPromises(); // backup status refreshed - expect(client.getKeyBackupVersion).toHaveBeenCalled(); + expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled(); expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled(); }); }); diff --git a/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx index 0ee882767bb..27403919b6c 100644 --- a/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx @@ -34,7 +34,6 @@ describe("", () => { ...mockClientMethodsCrypto(), getRooms: jest.fn().mockReturnValue([]), getIgnoredUsers: jest.fn(), - getKeyBackupVersion: jest.fn(), }); const sdkContext = new SdkContextClass(); diff --git a/test/unit-tests/stores/SetupEncryptionStore-test.ts b/test/unit-tests/stores/SetupEncryptionStore-test.ts index b9ab29b94b1..d3d0300a215 100644 --- a/test/unit-tests/stores/SetupEncryptionStore-test.ts +++ b/test/unit-tests/stores/SetupEncryptionStore-test.ts @@ -37,6 +37,7 @@ describe("SetupEncryptionStore", () => { getDeviceVerificationStatus: jest.fn(), isDehydrationSupported: jest.fn().mockResolvedValue(false), startDehydration: jest.fn(), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), } as unknown as Mocked; client.getCrypto.mockReturnValue(mockCrypto);