/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { act, render, waitFor } from "@testing-library/react";
import React, { ComponentProps } from "react";
import { User, TypedEventEmitter, Device, MatrixClient } from "matrix-js-sdk/src/matrix";
import { mocked, Mocked } from "jest-mock";
import {
EmojiMapping,
ShowSasCallbacks,
VerificationPhase as Phase,
VerificationRequest,
VerificationRequestEvent,
Verifier,
VerifierEvent,
VerifierEventHandlerMap,
} from "matrix-js-sdk/src/crypto-api";
import VerificationPanel from "../../../../src/components/views/right_panel/VerificationPanel";
import { flushPromises, stubClient } from "../../../test-utils";
describe("", () => {
let client: MatrixClient;
beforeEach(() => {
client = stubClient();
});
describe("'Ready' phase (dialog mode)", () => {
it("should show a 'Start' button", () => {
const container = renderComponent({
request: makeMockVerificationRequest({
phase: Phase.Ready,
}),
layout: "dialog",
});
container.getByRole("button", { name: "Start" });
});
it("should show a QR code if the other side can scan and QR bytes are calculated", async () => {
const request = makeMockVerificationRequest({
phase: Phase.Ready,
});
request.generateQRCode.mockResolvedValue(Buffer.from("test", "utf-8"));
const container = renderComponent({
request: request,
layout: "dialog",
});
container.getByText("Scan this unique code");
// it shows a spinner at first; wait for the update which makes it show the QR code
await waitFor(() => {
container.getByAltText("QR Code");
});
});
});
describe("'Ready' phase (regular mode)", () => {
it("should show a 'Verify by emoji' button", () => {
const container = renderComponent({
request: makeMockVerificationRequest({ phase: Phase.Ready }),
});
container.getByRole("button", { name: "Verify by emoji" });
});
it("should show a QR code if the other side can scan and QR bytes are calculated", async () => {
const request = makeMockVerificationRequest({
phase: Phase.Ready,
});
request.generateQRCode.mockResolvedValue(Buffer.from("test", "utf-8"));
const container = renderComponent({
request: request,
member: new User("@other:user"),
});
container.getByText("Ask @other:user to scan your code:");
// it shows a spinner at first; wait for the update which makes it show the QR code
await waitFor(() => {
container.getByAltText("QR Code");
});
});
});
describe("'Verify by emoji' flow", () => {
let mockVerifier: Mocked;
let mockRequest: Mocked;
beforeEach(() => {
mockVerifier = makeMockVerifier();
mockRequest = makeMockVerificationRequest({
verifier: mockVerifier as unknown as VerificationRequest["verifier"],
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.getShowSasCallbacks.mockReturnValue(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");
}
});
describe("'Verify own device' flow", () => {
beforeEach(() => {
Object.defineProperty(mockRequest, "isSelfVerification", { get: () => true });
Object.defineProperty(mockRequest, "otherDeviceId", { get: () => "other_device" });
const otherDeviceDetails = new Device({
algorithms: [],
deviceId: "other_device",
keys: new Map(),
userId: "",
displayName: "my other device",
});
mocked(client.getCrypto()!).getUserDeviceInfo.mockResolvedValue(
new Map([[client.getSafeUserId(), new Map([["other_device", otherDeviceDetails]])]]),
);
});
it("should show 'Waiting for you to verify' after confirming", async () => {
const rendered = renderComponent({
request: mockRequest,
phase: Phase.Started,
});
// wait for the device to be looked up
await act(() => flushPromises());
// fire the ShowSas event
const sasEvent = makeMockSasCallbacks();
mockVerifier.getShowSasCallbacks.mockReturnValue(sasEvent);
act(() => {
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
});
// confirm
act(() => {
rendered.getByRole("button", { name: "They match" }).click();
});
expect(rendered.container).toHaveTextContent(
"Waiting for you to verify on your other device, my other device (other_device)…",
);
});
});
});
});
function renderComponent(props: Partial> & { request: VerificationRequest }) {
const defaultProps = {
layout: "",
member: {} as User,
onClose: () => {},
isRoomEncrypted: false,
inDialog: false,
phase: props.request.phase,
};
return render();
}
function makeMockVerificationRequest(props: Partial = {}): Mocked {
const request = new TypedEventEmitter();
Object.assign(request, {
cancel: jest.fn(),
otherPartySupportsMethod: jest.fn().mockReturnValue(true),
generateQRCode: jest.fn().mockResolvedValue(undefined),
...props,
});
return request as unknown as Mocked;
}
function makeMockVerifier(): Mocked {
const verifier = new TypedEventEmitter();
Object.assign(verifier, {
cancel: jest.fn(),
verify: jest.fn(),
getShowSasCallbacks: jest.fn(),
getReciprocateQrCodeCallbacks: 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(),
};
}