diff --git a/src/components/views/dialogs/IncomingSasDialog.tsx b/src/components/views/dialogs/IncomingSasDialog.tsx index b0bb9cbc814..3772b689fa2 100644 --- a/src/components/views/dialogs/IncomingSasDialog.tsx +++ b/src/components/views/dialogs/IncomingSasDialog.tsx @@ -15,8 +15,8 @@ limitations under the License. */ import React, { ReactNode } from "react"; -import { IGeneratedSas, ISasEvent, SasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; -import { VerificationBase, VerificationEvent } from "matrix-js-sdk/src/crypto/verification/Base"; +import { VerificationBase } from "matrix-js-sdk/src/crypto/verification/Base"; +import { GeneratedSas, ShowSasCallbacks, VerifierEvent } from "matrix-js-sdk/src/crypto-api/verification"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -37,7 +37,7 @@ const PHASE_VERIFIED = 3; const PHASE_CANCELLED = 4; interface IProps { - verifier: VerificationBase; + verifier: VerificationBase; onFinished(verified?: boolean): void; } @@ -50,11 +50,11 @@ interface IState { displayname?: string; } | null; opponentProfileError: Error | null; - sas: IGeneratedSas | null; + sas: GeneratedSas | null; } export default class IncomingSasDialog extends React.Component { - private showSasEvent: ISasEvent | null; + private showSasEvent: ShowSasCallbacks | null; public constructor(props: IProps) { super(props); @@ -73,8 +73,8 @@ export default class IncomingSasDialog extends React.Component { opponentProfileError: null, sas: null, }; - this.props.verifier.on(SasEvent.ShowSas, this.onVerifierShowSas); - this.props.verifier.on(VerificationEvent.Cancel, this.onVerifierCancel); + this.props.verifier.on(VerifierEvent.ShowSas, this.onVerifierShowSas); + this.props.verifier.on(VerifierEvent.Cancel, this.onVerifierCancel); this.fetchOpponentProfile(); } @@ -82,7 +82,7 @@ export default class IncomingSasDialog extends React.Component { if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) { this.props.verifier.cancel(new Error("User cancel")); } - this.props.verifier.removeListener(SasEvent.ShowSas, this.onVerifierShowSas); + this.props.verifier.removeListener(VerifierEvent.ShowSas, this.onVerifierShowSas); } private async fetchOpponentProfile(): Promise { @@ -118,7 +118,7 @@ export default class IncomingSasDialog extends React.Component { }); }; - private onVerifierShowSas = (e: ISasEvent): void => { + private onVerifierShowSas = (e: ShowSasCallbacks): void => { this.showSasEvent = e; this.setState({ phase: PHASE_SHOW_SAS, diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index 1820309e05d..019d0affb96 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import { verificationMethods } from "matrix-js-sdk/src/crypto"; -import { QrCodeEvent, ReciprocateQRCode, SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; +import { ReciprocateQRCode, SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import { Phase, VerificationRequest, @@ -24,9 +24,10 @@ import { } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { User } from "matrix-js-sdk/src/models/user"; -import { SAS, SasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; +import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS"; import { logger } from "matrix-js-sdk/src/logger"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { ShowQrCodeCallbacks, ShowSasCallbacks, VerifierEvent } from "matrix-js-sdk/src/crypto-api/verification"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import VerificationQRCode from "../elements/crypto/VerificationQRCode"; @@ -48,10 +49,10 @@ interface IProps { } interface IState { - sasEvent?: SAS["sasEvent"]; + sasEvent?: ShowSasCallbacks; emojiButtonClicked?: boolean; reciprocateButtonClicked?: boolean; - reciprocateQREvent?: ReciprocateQRCode["reciprocateQREvent"]; + reciprocateQREvent?: ShowQrCodeCallbacks; } export default class VerificationPanel extends React.PureComponent { @@ -401,8 +402,8 @@ export default class VerificationPanel extends React.PureComponent void; onCancel: () => void; - sas: IGeneratedSas; + sas: GeneratedSas; isSelf?: boolean; inDialog?: boolean; // whether this component is being shown in a dialog and to use DialogButtons } diff --git a/test/components/views/dialogs/IncomingSasDialog-test.tsx b/test/components/views/dialogs/IncomingSasDialog-test.tsx new file mode 100644 index 00000000000..61b9f21e03b --- /dev/null +++ b/test/components/views/dialogs/IncomingSasDialog-test.tsx @@ -0,0 +1,83 @@ +/* +Copyright 2023 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 { act, render } from "@testing-library/react"; +import React from "react"; +import { Mocked } from "jest-mock"; +import { VerificationBase } from "matrix-js-sdk/src/crypto/verification/Base"; +import { + EmojiMapping, + ShowSasCallbacks, + VerifierEvent, + VerifierEventHandlerMap, +} from "matrix-js-sdk/src/crypto-api/verification"; +import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; + +import IncomingSasDialog from "../../../../src/components/views/dialogs/IncomingSasDialog"; +import { stubClient } from "../../../test-utils"; + +describe("IncomingSasDialog", () => { + beforeEach(() => { + stubClient(); + }); + + it("shows a spinner at first", () => { + const mockVerifier = makeMockVerifier(); + const { container } = renderComponent(mockVerifier); + expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy(); + }); + + it("should show some emojis once keys are exchanged", () => { + const mockVerifier = makeMockVerifier(); + const { container } = renderComponent(mockVerifier); + + // fire the ShowSas event + const sasEvent = makeMockSasCallbacks(); + act(() => { + mockVerifier.emit(VerifierEvent.ShowSas, sasEvent); + }); + + const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block"); + expect(emojis.length).toEqual(7); + for (const emoji of emojis) { + expect(emoji).toHaveTextContent("🦄Unicorn"); + } + }); +}); + +function renderComponent(verifier: VerificationBase, onFinished = () => true) { + return render(); +} + +function makeMockVerifier(): Mocked { + const verifier = new TypedEventEmitter(); + Object.assign(verifier, { + cancel: jest.fn(), + }); + return verifier as unknown as Mocked; +} + +function makeMockSasCallbacks(): ShowSasCallbacks { + const unicorn: EmojiMapping = ["🦄", "unicorn"]; + return { + sas: { + emoji: new Array(7).fill(unicorn), + }, + cancel: jest.fn(), + confirm: jest.fn(), + mismatch: jest.fn(), + }; +} diff --git a/test/components/views/right_panel/VerificationPanel-test.tsx b/test/components/views/right_panel/VerificationPanel-test.tsx new file mode 100644 index 00000000000..01fff54c011 --- /dev/null +++ b/test/components/views/right_panel/VerificationPanel-test.tsx @@ -0,0 +1,136 @@ +/* +Copyright 2023 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 { act, render } from "@testing-library/react"; +import React from "react"; +import { + Phase, + VerificationRequest, + VerificationRequestEvent, +} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter"; +import { User } from "matrix-js-sdk/src/models/user"; +import { Mocked } from "jest-mock"; +import { VerificationBase } from "matrix-js-sdk/src/crypto/verification/Base"; +import { + EmojiMapping, + ShowSasCallbacks, + VerifierEvent, + VerifierEventHandlerMap, +} from "matrix-js-sdk/src/crypto-api/verification"; +import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS"; +import { IVerificationChannel } from "matrix-js-sdk/src/crypto/verification/request/Channel"; + +import VerificationPanel from "../../../../src/components/views/right_panel/VerificationPanel"; +import { stubClient } from "../../../test-utils"; + +describe("", () => { + beforeEach(() => { + stubClient(); + }); + + it("should show a 'Verify by emoji' button", () => { + const container = renderComponent({ + request: makeMockVerificationRequest(), + phase: Phase.Ready, + }); + container.getByRole("button", { name: "Verify by emoji" }); + }); + + describe("'Verify by emoji' flow", () => { + let mockVerifier: Mocked; + let mockRequest: Mocked; + + beforeEach(() => { + mockVerifier = makeMockVerifier(); + mockRequest = makeMockVerificationRequest({ + verifier: mockVerifier, + chosenMethod: "m.sas.v1", + }); + }); + + it("shows a spinner initially", () => { + const { container } = renderComponent({ + request: mockRequest, + phase: Phase.Started, + }); + expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy(); + }); + + it("should show some emojis once keys are exchanged", () => { + const { container } = renderComponent({ + request: mockRequest, + phase: Phase.Started, + }); + + // fire the ShowSas event + const sasEvent = makeMockSasCallbacks(); + (mockVerifier as unknown as SAS).sasEvent = sasEvent; + act(() => { + mockVerifier.emit(VerifierEvent.ShowSas, sasEvent); + }); + + const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block"); + expect(emojis.length).toEqual(7); + for (const emoji of emojis) { + expect(emoji).toHaveTextContent("🦄Unicorn"); + } + }); + }); +}); + +function renderComponent(props: { request: VerificationRequest; phase: Phase }) { + const defaultProps = { + layout: "", + member: {} as User, + onClose: () => undefined, + isRoomEncrypted: false, + inDialog: false, + }; + return render(); +} + +function makeMockVerificationRequest(props: Partial = {}): Mocked { + const request = new TypedEventEmitter(); + Object.assign(request, { + channel: {} as IVerificationChannel, + cancel: jest.fn(), + otherPartySupportsMethod: jest.fn().mockReturnValue(true), + ...props, + }); + return request as unknown as Mocked; +} + +function makeMockVerifier(): Mocked { + const verifier = new TypedEventEmitter(); + Object.assign(verifier, { + cancel: jest.fn(), + verify: jest.fn(), + }); + return verifier as unknown as Mocked; +} + +function makeMockSasCallbacks(): ShowSasCallbacks { + const unicorn: EmojiMapping = ["🦄", "unicorn"]; + return { + sas: { + emoji: new Array(7).fill(unicorn), + }, + cancel: jest.fn(), + confirm: jest.fn(), + mismatch: jest.fn(), + }; +}