diff --git a/src/components/views/messages/MKeyVerificationConclusion.tsx b/src/components/views/messages/MKeyVerificationConclusion.tsx index 5fd8df699f..5c51d5c2ce 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.tsx +++ b/src/components/views/messages/MKeyVerificationConclusion.tsx @@ -18,6 +18,7 @@ import React from "react"; import classNames from "classnames"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { + Phase as VerificationPhase, VerificationRequest, VerificationRequestEvent, } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; @@ -78,11 +79,11 @@ export default class MKeyVerificationConclusion extends React.Component return false; } // .cancel event that was sent after the verification finished, ignore - if (mxEvent.getType() === EventType.KeyVerificationCancel && !request.cancelled) { + if (mxEvent.getType() === EventType.KeyVerificationCancel && request.phase !== VerificationPhase.Cancelled) { return false; } // .done event that was sent after the verification cancelled, ignore - if (mxEvent.getType() === EventType.KeyVerificationDone && !request.done) { + if (mxEvent.getType() === EventType.KeyVerificationDone && request.phase !== VerificationPhase.Done) { return false; } @@ -112,11 +113,11 @@ export default class MKeyVerificationConclusion extends React.Component let title: string | undefined; - if (request.done) { + if (request.phase === VerificationPhase.Done) { title = _t("You verified %(name)s", { name: getNameForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!), }); - } else if (request.cancelled) { + } else if (request.phase === VerificationPhase.Cancelled) { const userId = request.cancellingUserId; if (userId === myUserId) { title = _t("You cancelled verifying %(name)s", { @@ -131,7 +132,7 @@ export default class MKeyVerificationConclusion extends React.Component if (title) { const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", { - mx_cryptoEvent_icon_verified: request.done, + mx_cryptoEvent_icon_verified: request.phase === VerificationPhase.Done, }); return ( { const { mxEvent } = this.props; const request = mxEvent.verificationRequest; - if (!request || request.invalid) { + if (!request || request.phase === VerificationPhase.Unsent) { return null; } @@ -138,14 +141,17 @@ export default class MKeyVerificationRequest extends React.Component { if (!request.canAccept) { let stateLabel; - const accepted = request.ready || request.started || request.done; + const accepted = + request.phase === VerificationPhase.Ready || + request.phase === VerificationPhase.Started || + request.phase === VerificationPhase.Done; if (accepted) { stateLabel = ( {this.acceptedLabel(request.receivingUserId)} ); - } else if (request.cancelled) { + } else if (request.phase === VerificationPhase.Cancelled) { stateLabel = this.cancelledLabel(request.cancellingUserId!); } else if (request.accepting) { stateLabel = _t("Accepting…"); diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index 12e37bba85..73c2ba02bd 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -18,6 +18,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { PHASE_REQUESTED, PHASE_UNSENT, + Phase as VerificationPhase, VerificationRequest, VerificationRequestEvent, } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; @@ -76,7 +77,7 @@ const EncryptionPanel: React.FC = (props: IProps) => { }, [verificationRequestPromise]); const changeHandler = useCallback(() => { // handle transitions -> cancelled for mismatches which fire a modal instead of showing a card - if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) { + if (request && request.phase === VerificationPhase.Cancelled && MISMATCHES.includes(request.cancellationCode)) { Modal.createDialog(ErrorDialog, { headerImage: require("../../../../res/img/e2e/warning-deprecated.svg").default, title: _t("Your messages are not secure"), diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index d370d5d2ff..d9300c6dc5 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -17,6 +17,7 @@ limitations under the License. import EventEmitter from "events"; import { PHASE_DONE as VERIF_PHASE_DONE, + Phase as VerificationPhase, VerificationRequest, VerificationRequestEvent, } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; @@ -178,7 +179,7 @@ export class SetupEncryptionStore extends EventEmitter { }; public onVerificationRequestChange = async (): Promise => { - if (this.verificationRequest?.cancelled) { + if (this.verificationRequest?.phase === VerificationPhase.Cancelled) { this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange); this.verificationRequest = null; this.emit("update"); diff --git a/test/components/views/messages/MKeyVerificationConclusion-test.tsx b/test/components/views/messages/MKeyVerificationConclusion-test.tsx index c5d023c775..b9cb024e73 100644 --- a/test/components/views/messages/MKeyVerificationConclusion-test.tsx +++ b/test/components/views/messages/MKeyVerificationConclusion-test.tsx @@ -17,10 +17,13 @@ limitations under the License. import React from "react"; import { render } from "@testing-library/react"; import { EventEmitter } from "events"; -import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; -import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { + Phase as VerificationPhase, + VerificationRequest, +} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import MKeyVerificationConclusion from "../../../../src/components/views/messages/MKeyVerificationConclusion"; @@ -38,27 +41,32 @@ describe("MKeyVerificationConclusion", () => { }); const getMockVerificationRequest = ({ - pending, - cancelled, - done, + pending = false, + phase = VerificationPhase.Unsent, otherUserId, + cancellingUserId, }: { pending?: boolean; - cancelled?: boolean; - done?: boolean; + phase?: VerificationPhase; otherUserId?: string; + cancellingUserId?: string; }) => { class MockVerificationRequest extends EventEmitter { constructor( - public readonly pending?: boolean, - public readonly cancelled?: boolean, - public readonly done?: boolean, + public readonly pending: boolean, + public readonly phase: VerificationPhase, public readonly otherUserId?: string, + public readonly cancellingUserId?: string, ) { super(); } } - return new MockVerificationRequest(pending, cancelled, done, otherUserId) as unknown as VerificationRequest; + return new MockVerificationRequest( + pending, + phase, + otherUserId, + cancellingUserId, + ) as unknown as VerificationRequest; }; beforeEach(() => { @@ -85,14 +93,14 @@ describe("MKeyVerificationConclusion", () => { it("shouldn't render if the event type is cancel but the request type isn't", () => { const event = new MatrixEvent({ type: EventType.KeyVerificationCancel }); - event.verificationRequest = getMockVerificationRequest({ cancelled: false }); + event.verificationRequest = getMockVerificationRequest({}); const { container } = render(); expect(container).toBeEmpty(); }); it("shouldn't render if the event type is done but the request type isn't", () => { const event = new MatrixEvent({ type: "m.key.verification.done" }); - event.verificationRequest = getMockVerificationRequest({ done: false }); + event.verificationRequest = getMockVerificationRequest({}); const { container } = render(); expect(container).toBeEmpty(); }); @@ -101,7 +109,7 @@ describe("MKeyVerificationConclusion", () => { mockClient.checkUserTrust.mockReturnValue(untrustworthy); const event = new MatrixEvent({ type: "m.key.verification.done" }); - event.verificationRequest = getMockVerificationRequest({ done: true }); + event.verificationRequest = getMockVerificationRequest({ phase: VerificationPhase.Done }); const { container } = render(); expect(container).toBeEmpty(); }); @@ -110,7 +118,10 @@ describe("MKeyVerificationConclusion", () => { mockClient.checkUserTrust.mockReturnValue(untrustworthy); const event = new MatrixEvent({ type: "m.key.verification.done" }); - event.verificationRequest = getMockVerificationRequest({ done: true, otherUserId: "@someuser:domain" }); + event.verificationRequest = getMockVerificationRequest({ + phase: VerificationPhase.Done, + otherUserId: "@someuser:domain", + }); const { container } = render(); expect(container).toBeEmpty(); @@ -132,4 +143,14 @@ describe("MKeyVerificationConclusion", () => { ); expect(container).not.toBeEmpty(); }); + + it("should render appropriately if we cancelled the verification", () => { + const event = new MatrixEvent({ type: "m.key.verification.cancel" }); + event.verificationRequest = getMockVerificationRequest({ + phase: VerificationPhase.Cancelled, + cancellingUserId: userId, + }); + const { container } = render(); + expect(container).toHaveTextContent("You cancelled verifying"); + }); }); diff --git a/test/components/views/messages/MKeyVerificationRequest-test.tsx b/test/components/views/messages/MKeyVerificationRequest-test.tsx new file mode 100644 index 0000000000..e3d0d19644 --- /dev/null +++ b/test/components/views/messages/MKeyVerificationRequest-test.tsx @@ -0,0 +1,87 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render } from "@testing-library/react"; +import { EventEmitter } from "events"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { + Phase as VerificationPhase, + VerificationRequest, +} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; + +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils"; +import MKeyVerificationRequest from "../../../../src/components/views/messages/MKeyVerificationRequest"; + +describe("MKeyVerificationRequest", () => { + const userId = "@user:server"; + const getMockVerificationRequest = (props: Partial) => { + const res = new EventEmitter(); + Object.assign(res, { + phase: VerificationPhase.Requested, + canAccept: false, + initiatedByMe: true, + ...props, + }); + return res as unknown as VerificationRequest; + }; + + beforeEach(() => { + jest.clearAllMocks(); + getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + getRoom: jest.fn(), + }); + }); + + afterAll(() => { + jest.spyOn(MatrixClientPeg, "get").mockRestore(); + }); + + it("should not render if the request is absent", () => { + const event = new MatrixEvent({ type: "m.key.verification.request" }); + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should not render if the request is unsent", () => { + const event = new MatrixEvent({ type: "m.key.verification.request" }); + event.verificationRequest = getMockVerificationRequest({ + phase: VerificationPhase.Unsent, + }); + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should render appropriately when the request was sent", () => { + const event = new MatrixEvent({ type: "m.key.verification.request" }); + event.verificationRequest = getMockVerificationRequest({}); + const { container } = render(); + expect(container).toHaveTextContent("You sent a verification request"); + }); + + it("should render appropriately when the request was cancelled", () => { + const event = new MatrixEvent({ type: "m.key.verification.request" }); + event.verificationRequest = getMockVerificationRequest({ + phase: VerificationPhase.Cancelled, + cancellingUserId: userId, + }); + const { container } = render(); + expect(container).toHaveTextContent("You sent a verification request"); + expect(container).toHaveTextContent("You cancelled"); + }); +}); diff --git a/test/components/views/right_panel/UserInfo-test.tsx b/test/components/views/right_panel/UserInfo-test.tsx index bf1771c515..95c0fff74c 100644 --- a/test/components/views/right_panel/UserInfo-test.tsx +++ b/test/components/views/right_panel/UserInfo-test.tsx @@ -28,10 +28,15 @@ import { CryptoApi, DeviceVerificationStatus, } from "matrix-js-sdk/src/matrix"; -import { Phase, VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { + Phase, + VerificationRequest, + VerificationRequestEvent, +} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { Device } from "matrix-js-sdk/src/models/device"; import { defer } from "matrix-js-sdk/src/utils"; +import { EventEmitter } from "events"; import UserInfo, { BanToggleButton, @@ -55,6 +60,7 @@ import Modal from "../../../../src/Modal"; import { E2EStatus } from "../../../../src/utils/ShieldUtils"; import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/direct-messages"; import { clearAllModals, flushPromises } from "../../../test-utils"; +import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog"; jest.mock("../../../../src/utils/direct-messages", () => ({ ...jest.requireActual("../../../../src/utils/direct-messages"), @@ -163,14 +169,21 @@ beforeEach(() => { }); describe("", () => { - const verificationRequest = { - pending: true, - on: jest.fn(), - phase: Phase.Ready, - channel: { transactionId: 1 }, - otherPartySupportsMethod: jest.fn(), - off: jest.fn(), - } as unknown as VerificationRequest; + class MockVerificationRequest extends EventEmitter { + pending = true; + phase: Phase = Phase.Ready; + cancellationCode: string | null = null; + + constructor(opts: Partial) { + super(); + Object.assign(this, { + channel: { transactionId: 1 }, + otherPartySupportsMethod: jest.fn(), + ...opts, + }); + } + } + let verificationRequest: MockVerificationRequest; const defaultProps = { user: defaultUser, @@ -189,6 +202,15 @@ describe("", () => { }); }; + beforeEach(() => { + verificationRequest = new MockVerificationRequest({}); + }); + + afterEach(async () => { + await clearAllModals(); + jest.clearAllMocks(); + }); + it("closes on close button click", async () => { renderComponent(); @@ -222,6 +244,42 @@ describe("", () => { expect(screen.getByText(/try with a different client/i)).toBeInTheDocument(); }); + it("should show error modal when the verification request is cancelled with a mismatch", () => { + renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest }); + + const spy = jest.spyOn(Modal, "createDialog"); + act(() => { + verificationRequest.phase = Phase.Cancelled; + verificationRequest.cancellationCode = "m.key_mismatch"; + verificationRequest.emit(VerificationRequestEvent.Change); + }); + expect(spy).toHaveBeenCalledWith( + ErrorDialog, + expect.objectContaining({ title: "Your messages are not secure" }), + ); + }); + + it("should not show error modal when the verification request is changed for some other reason", () => { + renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest }); + + const spy = jest.spyOn(Modal, "createDialog"); + + // change to "started" + act(() => { + verificationRequest.phase = Phase.Started; + verificationRequest.emit(VerificationRequestEvent.Change); + }); + + // cancelled for some other reason + act(() => { + verificationRequest.phase = Phase.Cancelled; + verificationRequest.cancellationCode = "changed my mind"; + verificationRequest.emit(VerificationRequestEvent.Change); + }); + + expect(spy).not.toHaveBeenCalled(); + }); + it("renders close button correctly when encryption panel with a pending verification request", () => { renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest }); expect(screen.getByTestId("base-card-close-button")).toHaveAttribute("title", "Cancel");