diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts index b5db3871b84..eb0e7628290 100644 --- a/cypress/e2e/crypto/crypto.spec.ts +++ b/cypress/e2e/crypto/crypto.spec.ts @@ -14,17 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - +import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; +import type { CypressBot } from "../../support/bot"; import { SynapseInstance } from "../../plugins/synapsedocker"; import Chainable = Cypress.Chainable; type EmojiMapping = [emoji: string, name: string]; interface CryptoTestContext extends Mocha.Context { synapse: SynapseInstance; - bob: MatrixClient; + bob: CypressBot; } const waitForVerificationRequest = (cli: MatrixClient): Promise => { @@ -197,7 +197,7 @@ describe("Cryptography", function () { cy.bootstrapCrossSigning(); autoJoin(this.bob); - /* we need to have a room with the other user present, so we can open the verification panel */ + // we need to have a room with the other user present, so we can open the verification panel let roomId: string; cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then((_room1Id) => { roomId = _room1Id; @@ -210,4 +210,85 @@ describe("Cryptography", function () { verify.call(this); }); + + it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) { + cy.bootstrapCrossSigning(); + + // bob has a second, not cross-signed, device + cy.loginBot(this.synapse, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice"); + + autoJoin(this.bob); + + // first create the room, so that we can open the verification panel + cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }) + .as("testRoomId") + .then((roomId) => { + cy.log(`Created test room ${roomId}`); + cy.visit(`/#/room/${roomId}`); + + // enable encryption + cy.getClient().then((cli) => { + cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" }); + }); + + // wait for Bob to join the room, otherwise our attempt to open his user details may race + // with his join. + cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist"); + }); + + verify.call(this); + + cy.get("@testRoomId").then((roomId) => { + // bob sends a valid event + cy.wrap(this.bob.sendTextMessage(roomId, "Hoo!")).as("testEvent"); + + // the message should appear, decrypted, with no warning + cy.contains(".mx_EventTile_body", "Hoo!") + .closest(".mx_EventTile") + .should("have.class", "mx_EventTile_verified") + .should("not.have.descendants", ".mx_EventTile_e2eIcon_warning"); + + // bob sends an edit to the first message with his unverified device + cy.get("@bobSecondDevice").then((bobSecondDevice) => { + cy.get("@testEvent").then((testEvent) => { + bobSecondDevice.sendMessage(roomId, { + "m.new_content": { + msgtype: "m.text", + body: "Haa!", + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: testEvent.event_id, + }, + }); + }); + }); + + // the edit should have a warning + cy.contains(".mx_EventTile_body", "Haa!") + .closest(".mx_EventTile") + .within(() => { + cy.get(".mx_EventTile_e2eIcon_warning").should("exist"); + }); + + // a second edit from the verified device should be ok + cy.get("@testEvent").then((testEvent) => { + this.bob.sendMessage(roomId, { + "m.new_content": { + msgtype: "m.text", + body: "Hee!", + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: testEvent.event_id, + }, + }); + }); + + cy.contains(".mx_EventTile_body", "Hee!") + .closest(".mx_EventTile") + .should("have.class", "mx_EventTile_verified") + .should("not.have.descendants", ".mx_EventTile_e2eIcon_warning"); + }); + }); }); diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts index 78c83c5c8e0..750cd566bb7 100644 --- a/cypress/support/bot.ts +++ b/cypress/support/bot.ts @@ -51,6 +51,10 @@ const defaultCreateBotOptions = { bootstrapCrossSigning: true, } as CreateBotOpts; +export interface CypressBot extends MatrixClient { + __cypress_password: string; +} + declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { @@ -60,7 +64,7 @@ declare global { * @param synapse the instance on which to register the bot user * @param opts create bot options */ - getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable; + getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable; /** * Returns a new Bot instance logged in as an existing user * @param synapse the instance on which to register the bot user @@ -156,14 +160,20 @@ function setupBotClient( }); } -Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable => { +Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable => { opts = Object.assign({}, defaultCreateBotOptions, opts); const username = Cypress._.uniqueId(opts.userIdPrefix); const password = Cypress._.uniqueId("password_"); - return cy.registerUser(synapse, username, password, opts.displayName).then((credentials) => { - cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`); - return setupBotClient(synapse, credentials, opts); - }); + return cy + .registerUser(synapse, username, password, opts.displayName) + .then((credentials) => { + cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`); + return setupBotClient(synapse, credentials, opts); + }) + .then((client): Chainable => { + Object.assign(client, { __cypress_password: password }); + return cy.wrap(client as CypressBot); + }); }); Cypress.Commands.add( diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index e34bf8716f6..711b6e315d6 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -232,7 +232,7 @@ interface IState { // Whether the action bar is focused. actionBarFocused: boolean; // Whether the event's sender has been verified. - verified: string; + verified: string | null; // The Relations model from the JS SDK for reactions to `mxEvent` reactions?: Relations | null | undefined; @@ -278,7 +278,8 @@ export class UnwrappedEventTile extends React.Component this.state = { // Whether the action bar is focused. actionBarFocused: false, - // Whether the event's sender has been verified. + // Whether the event's sender has been verified. `null` if no attempt has yet been made to verify + // (including if the event is not encrypted). verified: null, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), @@ -371,6 +372,7 @@ export class UnwrappedEventTile extends React.Component client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); this.props.mxEvent.on(MatrixEventEvent.Decrypted, this.onDecrypted); + this.props.mxEvent.on(MatrixEventEvent.Replaced, this.onReplaced); DecryptionFailureTracker.instance.addVisibleEvent(this.props.mxEvent); if (this.props.showReactions) { this.props.mxEvent.on(MatrixEventEvent.RelationsCreated, this.onReactionsCreated); @@ -395,7 +397,7 @@ export class UnwrappedEventTile extends React.Component const room = client.getRoom(this.props.mxEvent.getRoomId()); room?.on(ThreadEvent.New, this.onNewThread); - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(); } private get supportsThreadNotifications(): boolean { @@ -461,6 +463,7 @@ export class UnwrappedEventTile extends React.Component } this.isListeningForReceipts = false; this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted); + this.props.mxEvent.removeListener(MatrixEventEvent.Replaced, this.onReplaced); if (this.props.showReactions) { this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated); } @@ -470,7 +473,11 @@ export class UnwrappedEventTile extends React.Component this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate); } - public componentDidUpdate(prevProps: Readonly) { + public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { + // If the verification state changed, the height might have changed + if (prevState.verified !== this.state.verified && this.props.onHeightChanged) { + this.props.onHeightChanged(); + } // If we're not listening for receipts and expect to be, register a listener. if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) { MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt); @@ -478,7 +485,7 @@ export class UnwrappedEventTile extends React.Component } // re-check the sender verification as outgoing events progress through the send process. if (prevProps.eventSendStatus !== this.props.eventSendStatus) { - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(); } } @@ -586,26 +593,36 @@ export class UnwrappedEventTile extends React.Component */ private onDecrypted = () => { // we need to re-verify the sending device. - // (we call onHeightChanged in verifyEvent to handle the case where decryption - // has caused a change in size of the event tile) - this.verifyEvent(this.props.mxEvent); - this.forceUpdate(); + this.verifyEvent(); + // decryption might, of course, trigger a height change, so call onHeightChanged after the re-render + this.forceUpdate(this.props.onHeightChanged); }; private onDeviceVerificationChanged = (userId: string, device: string): void => { if (userId === this.props.mxEvent.getSender()) { - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(); } }; private onUserVerificationChanged = (userId: string, _trustStatus: UserTrustLevel): void => { if (userId === this.props.mxEvent.getSender()) { - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(); } }; - private async verifyEvent(mxEvent: MatrixEvent): Promise { + /** called when the event is edited after we show it. */ + private onReplaced = () => { + // re-verify the event if it is replaced (the edit may not be verified) + this.verifyEvent(); + }; + + private verifyEvent(): void { + // if the event was edited, show the verification info for the edit, not + // the original + const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; + if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) { + this.setState({ verified: null }); return; } @@ -615,12 +632,7 @@ export class UnwrappedEventTile extends React.Component if (encryptionInfo.mismatchedSender) { // something definitely wrong is going on here - this.setState( - { - verified: E2EState.Warning, - }, - this.props.onHeightChanged, - ); // Decryption may have caused a change in size + this.setState({ verified: E2EState.Warning }); return; } @@ -628,53 +640,28 @@ export class UnwrappedEventTile extends React.Component // If the message is unauthenticated, then display a grey // shield, otherwise if the user isn't cross-signed then // nothing's needed - this.setState( - { - verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated, - }, - this.props.onHeightChanged, - ); // Decryption may have caused a change in size + this.setState({ verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated }); return; } const eventSenderTrust = encryptionInfo.sender && MatrixClientPeg.get().checkDeviceTrust(senderId, encryptionInfo.sender.deviceId); if (!eventSenderTrust) { - this.setState( - { - verified: E2EState.Unknown, - }, - this.props.onHeightChanged, - ); // Decryption may have caused a change in size + this.setState({ verified: E2EState.Unknown }); return; } if (!eventSenderTrust.isVerified()) { - this.setState( - { - verified: E2EState.Warning, - }, - this.props.onHeightChanged, - ); // Decryption may have caused a change in size + this.setState({ verified: E2EState.Warning }); return; } if (!encryptionInfo.authenticated) { - this.setState( - { - verified: E2EState.Unauthenticated, - }, - this.props.onHeightChanged, - ); // Decryption may have caused a change in size + this.setState({ verified: E2EState.Unauthenticated }); return; } - this.setState( - { - verified: E2EState.Verified, - }, - this.props.onHeightChanged, - ); // Decryption may have caused a change in size + this.setState({ verified: E2EState.Verified }); } private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean { @@ -768,7 +755,9 @@ export class UnwrappedEventTile extends React.Component }; private renderE2EPadlock() { - const ev = this.props.mxEvent; + // if the event was edited, show the verification info for the edit, not + // the original + const ev = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; // no icon for local rooms if (isLocalRoom(ev.getRoomId()!)) return; diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx index bebec2efc84..1f347b9e4a5 100644 --- a/test/components/views/rooms/EventTile-test.tsx +++ b/test/components/views/rooms/EventTile-test.tsx @@ -14,19 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import * as React from "react"; import { act, render, screen, waitFor } from "@testing-library/react"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; +import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { IEncryptedEventInfo } from "matrix-js-sdk/src/crypto/api"; import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../src/settings/SettingsStore"; -import { getRoomContext, mkEvent, mkMessage, stubClient } from "../../../test-utils"; +import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; describe("EventTile", () => { @@ -34,6 +37,7 @@ describe("EventTile", () => { let mxEvent: MatrixEvent; let room: Room; let client: MatrixClient; + // let changeEvent: (event: MatrixEvent) => void; function TestEventTile(props: Partial) { @@ -67,7 +71,7 @@ describe("EventTile", () => { stubClient(); client = MatrixClientPeg.get(); - room = new Room(ROOM_ID, client, client.getUserId(), { + room = new Room(ROOM_ID, client, client.getUserId()!, { pendingEventOrdering: PendingEventOrdering.Detached, }); @@ -140,18 +144,194 @@ describe("EventTile", () => { expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0); act(() => { - room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Total, 3); + room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Total, 3); }); expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1); expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(0); act(() => { - room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Highlight, 1); + room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Highlight, 1); }); expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1); expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(1); }); }); + + describe("Event verification", () => { + // data for our stubbed getEventEncryptionInfo: a map from event id to result + const eventToEncryptionInfoMap = new Map(); + + const TRUSTED_DEVICE = DeviceInfo.fromStorage({}, "TRUSTED_DEVICE"); + const UNTRUSTED_DEVICE = DeviceInfo.fromStorage({}, "UNTRUSTED_DEVICE"); + + beforeEach(() => { + eventToEncryptionInfoMap.clear(); + + // a mocked version of getEventEncryptionInfo which will pick its result from `eventToEncryptionInfoMap` + client.getEventEncryptionInfo = (event) => eventToEncryptionInfoMap.get(event.getId()!)!; + + // a mocked version of checkUserTrust which always says the user is trusted (we do our testing via + // unverified devices). + const trustedUserTrustLevel = new UserTrustLevel(true, true, true); + client.checkUserTrust = (_userId) => trustedUserTrustLevel; + + // a version of checkDeviceTrust which says that TRUSTED_DEVICE is trusted, and others are not. + const trustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, true, false); + const untrustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, false, false); + client.checkDeviceTrust = (userId, deviceId) => { + if (deviceId === TRUSTED_DEVICE.deviceId) { + return trustedDeviceTrustLevel; + } else { + return untrustedDeviceTrustLevel; + } + }; + }); + + it("shows a warning for an event from an unverified device", async () => { + mxEvent = await mkEncryptedEvent({ + plainContent: { msgtype: "m.text", body: "msg1" }, + plainType: "m.room.message", + user: "@alice:example.org", + room: room.roomId, + }); + eventToEncryptionInfoMap.set(mxEvent.getId()!, { + authenticated: true, + sender: UNTRUSTED_DEVICE, + } as IEncryptedEventInfo); + + const { container } = getComponent(); + + const eventTiles = container.getElementsByClassName("mx_EventTile"); + expect(eventTiles).toHaveLength(1); + const eventTile = eventTiles[0]; + + expect(eventTile.classList).toContain("mx_EventTile_unverified"); + + // there should be a warning shield + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1); + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain( + "mx_EventTile_e2eIcon_warning", + ); + }); + + it("shows no shield for a verified event", async () => { + mxEvent = await mkEncryptedEvent({ + plainContent: { msgtype: "m.text", body: "msg1" }, + plainType: "m.room.message", + user: "@alice:example.org", + room: room.roomId, + }); + eventToEncryptionInfoMap.set(mxEvent.getId()!, { + authenticated: true, + sender: TRUSTED_DEVICE, + } as IEncryptedEventInfo); + + const { container } = getComponent(); + + const eventTiles = container.getElementsByClassName("mx_EventTile"); + expect(eventTiles).toHaveLength(1); + const eventTile = eventTiles[0]; + + expect(eventTile.classList).toContain("mx_EventTile_verified"); + + // there should be no warning + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0); + }); + + it("should update the warning when the event is edited", async () => { + // we start out with an event from the trusted device + mxEvent = await mkEncryptedEvent({ + plainContent: { msgtype: "m.text", body: "msg1" }, + plainType: "m.room.message", + user: "@alice:example.org", + room: room.roomId, + }); + eventToEncryptionInfoMap.set(mxEvent.getId()!, { + authenticated: true, + sender: TRUSTED_DEVICE, + } as IEncryptedEventInfo); + + const { container } = getComponent(); + + const eventTiles = container.getElementsByClassName("mx_EventTile"); + expect(eventTiles).toHaveLength(1); + const eventTile = eventTiles[0]; + + expect(eventTile.classList).toContain("mx_EventTile_verified"); + + // there should be no warning + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0); + + // then we replace the event with one from the unverified device + const replacementEvent = await mkEncryptedEvent({ + plainContent: { msgtype: "m.text", body: "msg1" }, + plainType: "m.room.message", + user: "@alice:example.org", + room: room.roomId, + }); + eventToEncryptionInfoMap.set(replacementEvent.getId()!, { + authenticated: true, + sender: UNTRUSTED_DEVICE, + } as IEncryptedEventInfo); + + act(() => { + mxEvent.makeReplaced(replacementEvent); + }); + + // check it was updated + expect(eventTile.classList).toContain("mx_EventTile_unverified"); + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1); + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain( + "mx_EventTile_e2eIcon_warning", + ); + }); + + it("should update the warning when the event is replaced with an unencrypted one", async () => { + jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true); + + // we start out with an event from the trusted device + mxEvent = await mkEncryptedEvent({ + plainContent: { msgtype: "m.text", body: "msg1" }, + plainType: "m.room.message", + user: "@alice:example.org", + room: room.roomId, + }); + eventToEncryptionInfoMap.set(mxEvent.getId()!, { + authenticated: true, + sender: TRUSTED_DEVICE, + } as IEncryptedEventInfo); + + const { container } = getComponent(); + + const eventTiles = container.getElementsByClassName("mx_EventTile"); + expect(eventTiles).toHaveLength(1); + const eventTile = eventTiles[0]; + + expect(eventTile.classList).toContain("mx_EventTile_verified"); + + // there should be no warning + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0); + + // then we replace the event with an unencrypted one + const replacementEvent = await mkMessage({ + msg: "msg2", + user: "@alice:example.org", + room: room.roomId, + event: true, + }); + + act(() => { + mxEvent.makeReplaced(replacementEvent); + }); + + // check it was updated + expect(eventTile.classList).not.toContain("mx_EventTile_verified"); + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1); + expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain( + "mx_EventTile_e2eIcon_warning", + ); + }); + }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 21de3983fb9..3baf9fe3c2e 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -38,6 +38,8 @@ import { normalize } from "matrix-js-sdk/src/utils"; import { ReEmitter } from "matrix-js-sdk/src/ReEmitter"; import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler"; import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; +import { CryptoBackend } from "matrix-js-sdk/src/common-crypto/CryptoBackend"; +import { IEventDecryptionResult } from "matrix-js-sdk/src/@types/crypto"; import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg"; @@ -316,26 +318,48 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent { } /** - * Create an m.presence event. - * @param {Object} opts Values for the presence. - * @return {Object|MatrixEvent} The event + * Create an m.room.encrypted event + * + * @param opts - Values for the event + * @param opts.room - The ID of the room for the event + * @param opts.user - The sender of the event + * @param opts.plainType - The type the event will have, once it has been decrypted + * @param opts.plainContent - The content the event will have, once it has been decrypted */ -export function mkPresence(opts) { - if (!opts.user) { - throw new Error("Missing user"); - } - const event = { - event_id: "$" + Math.random() + "-" + Math.random(), - type: "m.presence", - sender: opts.user, - content: { - avatar_url: opts.url, - displayname: opts.name, - last_active_ago: opts.ago, - presence: opts.presence || "offline", +export async function mkEncryptedEvent(opts: { + room: Room["roomId"]; + user: User["userId"]; + plainType: string; + plainContent: IContent; +}): Promise { + // we construct an event which has been decrypted by stubbing out CryptoBackend.decryptEvent and then + // calling MatrixEvent.attemptDecryption. + + const mxEvent = mkEvent({ + type: "m.room.encrypted", + room: opts.room, + user: opts.user, + event: true, + content: {}, + }); + + const decryptionResult: IEventDecryptionResult = { + claimedEd25519Key: "", + clearEvent: { + type: opts.plainType, + content: opts.plainContent, }, + forwardingCurve25519KeyChain: [], + senderCurve25519Key: "", + untrusted: false, }; - return opts.event ? new MatrixEvent(event) : event; + + const mockCrypto = { + decryptEvent: async (_ev): Promise => decryptionResult, + } as CryptoBackend; + + await mxEvent.attemptDecryption(mockCrypto); + return mxEvent; } /**