Merge branch 'develop' into florianduros/rip-out-legacy-crypto/migrate-roomview-isencrypted

This commit is contained in:
Florian Duros 2024-11-25 16:46:40 +01:00
commit 223dd9698c
No known key found for this signature in database
GPG key ID: A5BBB4041B493F15
26 changed files with 188 additions and 63 deletions

View file

@ -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

View file

@ -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 }}

88
.github/workflows/deploy.yml vendored Normal file
View file

@ -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 }}

View file

@ -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<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template);

View file

@ -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;

View file

@ -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 ||

View file

@ -230,12 +230,15 @@ export default class DeviceListener {
private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
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;

View file

@ -279,7 +279,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
if (!forceReset) {
try {
this.setState({ phase: Phase.Loading });
backupInfo = await cli.getKeyBackupVersion();
backupInfo = await crypto.getKeyBackupInfo();
} catch (e) {
logger.error("Error fetching backup data from server", e);
this.setState({ phase: Phase.LoadError });

View file

@ -1638,7 +1638,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} 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);

View file

@ -109,7 +109,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
}
// 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 });
}

View file

@ -258,7 +258,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
});
try {
const cli = MatrixClientPeg.safeGet();
const backupInfo = await cli.getKeyBackupVersion();
const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
const has4S = await cli.secretStorage.hasKey();
const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null;
this.setState({

View file

@ -275,7 +275,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
const content = this.props.mxEvent.getContent<ImageContent>();
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<IBodyProps, IState> {
}
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;
}

View file

@ -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);
},
});
};

View file

@ -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");

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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),
}),
});

View file

@ -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(),

View file

@ -96,12 +96,12 @@ describe("DeviceListener", () => {
}),
getSessionBackupPrivateKey: jest.fn(),
isEncryptionEnabledInRoom: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<CryptoApi>;
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 = [

View file

@ -139,6 +139,7 @@ describe("<MatrixChat />", () => {
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("<MatrixChat />", () => {
whoami: jest.fn(),
logout: jest.fn(),
getDeviceId: jest.fn(),
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
});
let mockClient: Mocked<MatrixClient>;
const serverConfig = {

View file

@ -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();

View file

@ -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.");
});

View file

@ -28,7 +28,7 @@ describe("<RestoreKeyBackupDialog />", () => {
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("<RestoreKeyBackupDialog />", () => {
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",

View file

@ -28,14 +28,13 @@ describe("<SecureBackupPanel />", () => {
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(),
getKeyBackupVersion: jest.fn().mockReturnValue("1"),
getClientWellKnown: jest.fn(),
});
const getComponent = () => render(<SecureBackupPanel />);
beforeEach(() => {
client.getKeyBackupVersion.mockResolvedValue({
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
version: "1",
algorithm: "test",
auth_data: {
@ -52,7 +51,6 @@ describe("<SecureBackupPanel />", () => {
});
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
client.getKeyBackupVersion.mockClear();
mocked(accessSecretStorage).mockClear().mockResolvedValue();
});
@ -65,8 +63,8 @@ describe("<SecureBackupPanel />", () => {
});
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("<SecureBackupPanel />", () => {
});
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("<SecureBackupPanel />", () => {
});
it("deletes backup after confirmation", async () => {
client.getKeyBackupVersion
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo")
.mockResolvedValueOnce({
version: "1",
algorithm: "test",
@ -157,7 +155,7 @@ describe("<SecureBackupPanel />", () => {
// 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("<SecureBackupPanel />", () => {
await flushPromises();
// backup status refreshed
expect(client.getKeyBackupVersion).toHaveBeenCalled();
expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled();
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
});
});

View file

@ -34,7 +34,6 @@ describe("<SecurityUserSettingsTab />", () => {
...mockClientMethodsCrypto(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getKeyBackupVersion: jest.fn(),
});
const sdkContext = new SdkContextClass();

View file

@ -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<CryptoApi>;
client.getCrypto.mockReturnValue(mockCrypto);