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: permissions:
checks: read checks: read
pages: write pages: write
deployments: write
env: env:
R2_BUCKET: "element-web-develop" R2_BUCKET: "element-web-develop"
R2_URL: ${{ vars.CF_R2_S3_API }} 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. // 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. // 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. // 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">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

View file

@ -22,6 +22,13 @@ declare module "matrix-js-sdk/src/types" {
[BLURHASH_FIELD]?: string; [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 { export interface StateEvents {
// Jitsi-backed video room state events // Jitsi-backed video room state events
[JitsiCallMemberEventType]: JitsiCallMemberContent; [JitsiCallMemberEventType]: JitsiCallMemberContent;

View file

@ -56,6 +56,7 @@ import { createThumbnail } from "./utils/image-media";
import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer"; import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer";
import { doMaybeLocalRoomAction } from "./utils/local-room"; import { doMaybeLocalRoomAction } from "./utils/local-room";
import { SdkContextClass } from "./contexts/SDKContext"; import { SdkContextClass } from "./contexts/SDKContext";
import { blobIsAnimated } from "./utils/Image.ts";
// scraped out of a macOS hidpi (5660ppm) screenshot png // scraped out of a macOS hidpi (5660ppm) screenshot png
// 5669 px (x-axis) , 5669 px (y-axis) , per metre // 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"; 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 imageElement = await loadImageElement(imageFile);
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType); const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
const imageInfo = result.info; 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 // For lesser supported image types, always include the thumbnail even if it is larger
if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) { 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. // 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 ( if (
// image is small enough already // image is small enough already
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||

View file

@ -230,12 +230,15 @@ export default class DeviceListener {
private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> { private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
if (!this.client) return null; if (!this.client) return null;
const now = new Date().getTime(); const now = new Date().getTime();
const crypto = this.client.getCrypto();
if (!crypto) return null;
if ( if (
!this.keyBackupInfo || !this.keyBackupInfo ||
!this.keyBackupFetchedAt || !this.keyBackupFetchedAt ||
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
) { ) {
this.keyBackupInfo = await this.client.getKeyBackupVersion(); this.keyBackupInfo = await crypto.getKeyBackupInfo();
this.keyBackupFetchedAt = now; this.keyBackupFetchedAt = now;
} }
return this.keyBackupInfo; return this.keyBackupInfo;

View file

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

View file

@ -1638,7 +1638,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else { } else {
// otherwise check the server to see if there's a new one // otherwise check the server to see if there's a new one
try { try {
newVersionInfo = await cli.getKeyBackupVersion(); newVersionInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
if (newVersionInfo !== null) haveNewVersion = true; if (newVersionInfo !== null) haveNewVersion = true;
} catch (e) { } catch (e) {
logger.error("Saw key backup error but failed to check backup version!", 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. // 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 }); 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 { try {
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
const backupInfo = await cli.getKeyBackupVersion(); const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
const has4S = await cli.secretStorage.hasKey(); const has4S = await cli.secretStorage.hasKey();
const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null; const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null;
this.setState({ this.setState({

View file

@ -275,7 +275,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
const content = this.props.mxEvent.getContent<ImageContent>(); 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 // 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. // 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 { try {
const blob = await this.props.mediaEventHelper!.sourceBlob.value; // If we didn't receive the MSC4230 is_animated flag
if (!(await blobIsAnimated(content.info?.mimetype, blob))) { // 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; isAnimated = false;
} }

View file

@ -118,7 +118,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
this.getUpdatedDiagnostics(); this.getUpdatedDiagnostics();
try { try {
const cli = MatrixClientPeg.safeGet(); 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 backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null; const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null;
@ -192,12 +192,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
if (!proceed) return; if (!proceed) return;
this.setState({ loading: true }); this.setState({ loading: true });
const versionToDelete = this.state.backupInfo!.version!; const versionToDelete = this.state.backupInfo!.version!;
MatrixClientPeg.safeGet() // deleteKeyBackupVersion fires a key backup status event
.getCrypto() // which will update the UI
?.deleteKeyBackupVersion(versionToDelete) MatrixClientPeg.safeGet().getCrypto()?.deleteKeyBackupVersion(versionToDelete);
.then(() => {
this.loadBackupStatus();
});
}, },
}); });
}; };

View file

@ -125,7 +125,7 @@ export class SetupEncryptionStore extends EventEmitter {
this.emit("update"); this.emit("update");
try { try {
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
const backupInfo = await cli.getKeyBackupVersion(); const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
this.backupInfo = backupInfo; this.backupInfo = backupInfo;
this.emit("update"); 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. 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"; import { BlurhashEncoder } from "../BlurhashEncoder";
@ -15,19 +15,7 @@ type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
interface IThumbnail { interface IThumbnail {
info: { info: ImageInfo;
thumbnail_info?: {
w: number;
h: number;
mimetype: string;
size: number;
};
w: number;
h: number;
[BLURHASH_FIELD]?: string;
thumbnail_url?: string;
thumbnail_file?: EncryptedFile;
};
thumbnail: Blob; thumbnail: Blob;
} }

View file

@ -474,10 +474,8 @@ export default class ElectronPlatform extends BasePlatform {
const url = super.getOidcCallbackUrl(); const url = super.getOidcCallbackUrl();
url.protocol = "io.element.desktop"; 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 // 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 if (url.href.startsWith(`${url.protocol}://`)) {
// field, so we cannot mutate `pathname` reliably and instead have to rewrite the href manually. url.href = url.href.replace("://", ":/");
if (url.pathname.startsWith("//")) {
url.href = url.href.replace(url.pathname, url.pathname.slice(1));
} }
return url; return url;
} }

View file

@ -143,7 +143,6 @@ export const mockClientMethodsCrypto = (): Partial<
> => ({ > => ({
isKeyBackupKeyStored: jest.fn(), isKeyBackupKeyStored: jest.fn(),
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
secretStorage: { hasKey: jest.fn() }, secretStorage: { hasKey: jest.fn() },
getCrypto: jest.fn().mockReturnValue({ getCrypto: jest.fn().mockReturnValue({
getUserDeviceInfo: jest.fn(), getUserDeviceInfo: jest.fn(),
@ -163,6 +162,7 @@ export const mockClientMethodsCrypto = (): Partial<
getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})), getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})),
getCrossSigningKeyId: jest.fn(), getCrossSigningKeyId: jest.fn(),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), 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" }] }), getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"), getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
credentials: { userId: "@userId:matrix.org" }, credentials: { userId: "@userId:matrix.org" },
getKeyBackupVersion: jest.fn(),
secretStorage: { secretStorage: {
get: jest.fn(), get: jest.fn(),
@ -135,6 +134,7 @@ export function createTestClient(): MatrixClient {
restoreKeyBackupWithPassphrase: jest.fn(), restoreKeyBackupWithPassphrase: jest.fn(),
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(), loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
storeSessionBackupPrivateKey: jest.fn(), storeSessionBackupPrivateKey: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
}), }),
getPushActionsForEvent: jest.fn(), getPushActionsForEvent: jest.fn(),

View file

@ -96,12 +96,12 @@ describe("DeviceListener", () => {
}), }),
getSessionBackupPrivateKey: jest.fn(), getSessionBackupPrivateKey: jest.fn(),
isEncryptionEnabledInRoom: jest.fn(), isEncryptionEnabledInRoom: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<CryptoApi>; } as unknown as Mocked<CryptoApi>;
mockClient = getMockClientWithEventEmitter({ mockClient = getMockClientWithEventEmitter({
isGuest: jest.fn(), isGuest: jest.fn(),
getUserId: jest.fn().mockReturnValue(userId), getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId), getSafeUserId: jest.fn().mockReturnValue(userId),
getKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
getRooms: jest.fn().mockReturnValue([]), getRooms: jest.fn().mockReturnValue([]),
isVersionSupported: jest.fn().mockResolvedValue(true), isVersionSupported: jest.fn().mockResolvedValue(true),
isInitialSyncComplete: jest.fn().mockReturnValue(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 () => { it("shows set up encryption toast when user has a key backup available", async () => {
// non falsy response // non falsy response
mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo); mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
await createAndStart(); await createAndStart();
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
@ -673,7 +673,7 @@ describe("DeviceListener", () => {
describe("When Room Key Backup is not enabled", () => { describe("When Room Key Backup is not enabled", () => {
beforeEach(() => { beforeEach(() => {
// no backup // no backup
mockClient.getKeyBackupVersion.mockResolvedValue(null); mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
}); });
it("Should report recovery state as Enabled", async () => { it("Should report recovery state as Enabled", async () => {
@ -722,7 +722,7 @@ describe("DeviceListener", () => {
}); });
// no backup // no backup
mockClient.getKeyBackupVersion.mockResolvedValue(null); mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
await createAndStart(); await createAndStart();
@ -872,7 +872,7 @@ describe("DeviceListener", () => {
describe("When Room Key Backup is enabled", () => { describe("When Room Key Backup is enabled", () => {
beforeEach(() => { beforeEach(() => {
// backup enabled - just need a mock object // backup enabled - just need a mock object
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
}); });
const testCases = [ const testCases = [

View file

@ -139,6 +139,7 @@ describe("<MatrixChat />", () => {
globalBlacklistUnverifiedDevices: false, globalBlacklistUnverifiedDevices: false,
// This needs to not finish immediately because we need to test the screen appears // This needs to not finish immediately because we need to test the screen appears
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
}), }),
secretStorage: { secretStorage: {
isStored: jest.fn().mockReturnValue(null), isStored: jest.fn().mockReturnValue(null),
@ -148,7 +149,6 @@ describe("<MatrixChat />", () => {
whoami: jest.fn(), whoami: jest.fn(),
logout: jest.fn(), logout: jest.fn(),
getDeviceId: jest.fn(), getDeviceId: jest.fn(),
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
}); });
let mockClient: Mocked<MatrixClient>; let mockClient: Mocked<MatrixClient>;
const serverConfig = { const serverConfig = {

View file

@ -22,7 +22,6 @@ describe("LogoutDialog", () => {
beforeEach(() => { beforeEach(() => {
mockClient = getMockClientWithEventEmitter({ mockClient = getMockClientWithEventEmitter({
...mockClientMethodsCrypto(), ...mockClientMethodsCrypto(),
getKeyBackupVersion: jest.fn(),
}); });
mockCrypto = mocked(mockClient.getCrypto()!); 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 () => { 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(); const rendered = renderComponent();
await rendered.findByText("Connect this session to Key Backup"); await rendered.findByText("Connect this session to Key Backup");
expect(rendered.container).toMatchSnapshot(); expect(rendered.container).toMatchSnapshot();
}); });
it("Prompts user to set up backup if there is no backup on the server", async () => { 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(); const rendered = renderComponent();
await rendered.findByText("Start using Key Backup"); await rendered.findByText("Start using Key Backup");
expect(rendered.container).toMatchSnapshot(); expect(rendered.container).toMatchSnapshot();
@ -69,7 +68,7 @@ describe("LogoutDialog", () => {
describe("when there is an error fetching backups", () => { describe("when there is an error fetching backups", () => {
filterConsole("Unable to fetch key backup status"); filterConsole("Unable to fetch key backup status");
it("prompts user to set up backup", async () => { it("prompts user to set up backup", async () => {
mockClient.getKeyBackupVersion.mockImplementation(async () => { mockCrypto.getKeyBackupInfo.mockImplementation(async () => {
throw new Error("beep"); throw new Error("beep");
}); });
const rendered = renderComponent(); const rendered = renderComponent();

View file

@ -77,7 +77,7 @@ describe("CreateSecretStorageDialog", () => {
filterConsole("Error fetching backup data from server"); filterConsole("Error fetching backup data from server");
it("shows an error", async () => { it("shows an error", async () => {
mockClient.getKeyBackupVersion.mockImplementation(async () => { jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
throw new Error("bleh bleh"); throw new Error("bleh bleh");
}); });
@ -92,7 +92,7 @@ describe("CreateSecretStorageDialog", () => {
expect(result.container).toMatchSnapshot(); expect(result.container).toMatchSnapshot();
// Now we can get the backup and we retry // 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 userEvent.click(screen.getByRole("button", { name: "Retry" }));
await screen.findByText("Your keys are now being backed up from this device."); await screen.findByText("Your keys are now being backed up from this device.");
}); });

View file

@ -28,7 +28,7 @@ describe("<RestoreKeyBackupDialog />", () => {
beforeEach(() => { beforeEach(() => {
matrixClient = stubClient(); matrixClient = stubClient();
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32)); 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 () => { it("should render", async () => {
@ -99,7 +99,7 @@ describe("<RestoreKeyBackupDialog />", () => {
test("should restore key backup when passphrase is filled", async () => { test("should restore key backup when passphrase is filled", async () => {
// Determine that the passphrase is required // Determine that the passphrase is required
jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
version: "1", version: "1",
auth_data: { auth_data: {
private_key_salt: "salt", private_key_salt: "salt",

View file

@ -28,14 +28,13 @@ describe("<SecureBackupPanel />", () => {
const client = getMockClientWithEventEmitter({ const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId), ...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(), ...mockClientMethodsCrypto(),
getKeyBackupVersion: jest.fn().mockReturnValue("1"),
getClientWellKnown: jest.fn(), getClientWellKnown: jest.fn(),
}); });
const getComponent = () => render(<SecureBackupPanel />); const getComponent = () => render(<SecureBackupPanel />);
beforeEach(() => { beforeEach(() => {
client.getKeyBackupVersion.mockResolvedValue({ jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
version: "1", version: "1",
algorithm: "test", algorithm: "test",
auth_data: { auth_data: {
@ -52,7 +51,6 @@ describe("<SecureBackupPanel />", () => {
}); });
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false); mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
client.getKeyBackupVersion.mockClear();
mocked(accessSecretStorage).mockClear().mockResolvedValue(); mocked(accessSecretStorage).mockClear().mockResolvedValue();
}); });
@ -65,8 +63,8 @@ describe("<SecureBackupPanel />", () => {
}); });
it("handles error fetching backup", async () => { it("handles error fetching backup", async () => {
// getKeyBackupVersion can fail for various reasons // getKeyBackupInfo can fail for various reasons
client.getKeyBackupVersion.mockImplementation(async () => { jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
throw new Error("beep beep"); throw new Error("beep beep");
}); });
const renderResult = getComponent(); const renderResult = getComponent();
@ -75,9 +73,9 @@ describe("<SecureBackupPanel />", () => {
}); });
it("handles absence of backup", async () => { it("handles absence of backup", async () => {
client.getKeyBackupVersion.mockResolvedValue(null); jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null);
getComponent(); getComponent();
// flush getKeyBackupVersion promise // flush getKeyBackupInfo promise
await flushPromises(); await flushPromises();
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument(); 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 () => { it("deletes backup after confirmation", async () => {
client.getKeyBackupVersion jest.spyOn(client.getCrypto()!, "getKeyBackupInfo")
.mockResolvedValueOnce({ .mockResolvedValueOnce({
version: "1", version: "1",
algorithm: "test", algorithm: "test",
@ -157,7 +155,7 @@ describe("<SecureBackupPanel />", () => {
// flush checkKeyBackup promise // flush checkKeyBackup promise
await flushPromises(); await flushPromises();
client.getKeyBackupVersion.mockClear(); jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear();
mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear(); mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
fireEvent.click(screen.getByText("Reset")); fireEvent.click(screen.getByText("Reset"));
@ -167,7 +165,7 @@ describe("<SecureBackupPanel />", () => {
await flushPromises(); await flushPromises();
// backup status refreshed // backup status refreshed
expect(client.getKeyBackupVersion).toHaveBeenCalled(); expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled();
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled(); expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
}); });
}); });

View file

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

View file

@ -37,6 +37,7 @@ describe("SetupEncryptionStore", () => {
getDeviceVerificationStatus: jest.fn(), getDeviceVerificationStatus: jest.fn(),
isDehydrationSupported: jest.fn().mockResolvedValue(false), isDehydrationSupported: jest.fn().mockResolvedValue(false),
startDehydration: jest.fn(), startDehydration: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<CryptoApi>; } as unknown as Mocked<CryptoApi>;
client.getCrypto.mockReturnValue(mockCrypto); client.getCrypto.mockReturnValue(mockCrypto);