Simplify references to VerificationRequest (#11045)

* Use new `VerificationRequest.getQRCodeBytes()`

* Use new `VerificationRequest.otherDeviceId`

* Remove references to `VerificationRequest.channel`

* Replace references to `VerificationRequest.{requesting,receiving}UserId`

Normally these are guarded by `request.initiatedByMe` so we can trivially
replace it with `request.otherUserId` or `client.getUserId()`. In one place we
actually need to apply some logic.

* increase test coverage

* Even more test coverage

* Even more test coverage
This commit is contained in:
Richard van der Hoff 2023-06-07 15:43:44 +01:00 committed by GitHub
parent ac2c9cef8d
commit 34439ee652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 310 additions and 33 deletions

View File

@ -1667,7 +1667,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
);
} else if (request.pending) {
ToastStore.sharedInstance().addOrReplaceToast({
key: "verifreq_" + request.channel.transactionId,
key: "verifreq_" + request.transactionId,
title: _t("Verification requested"),
icon: "verification",
props: { request },

View File

@ -15,19 +15,18 @@ limitations under the License.
*/
import React from "react";
import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
import QRCode from "../QRCode";
interface IProps {
qrCodeData: QRCodeData;
qrCodeBytes: Buffer;
}
export default class VerificationQRCode extends React.PureComponent<IProps> {
public render(): React.ReactNode {
return (
<QRCode
data={[{ data: this.props.qrCodeData.getBuffer(), mode: "byte" }]}
data={[{ data: this.props.qrCodeBytes, mode: "byte" }]}
className="mx_VerificationQRCode"
width={196}
/>

View File

@ -148,7 +148,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
if (accepted) {
stateLabel = (
<AccessibleButton onClick={this.openRequest}>
{this.acceptedLabel(request.receivingUserId)}
{this.acceptedLabel(request.initiatedByMe ? request.otherUserId : client.getSafeUserId())}
</AccessibleButton>
);
} else if (request.phase === VerificationPhase.Cancelled) {
@ -162,9 +162,9 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
}
if (!request.initiatedByMe) {
const name = getNameForEventRoom(client, request.requestingUserId, mxEvent.getRoomId()!);
const name = getNameForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!);
title = _t("%(name)s wants to verify", { name });
subtitle = userLabelForEventRoom(client, request.requestingUserId, mxEvent.getRoomId()!);
subtitle = userLabelForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!);
if (request.canAccept) {
stateNode = (
<div className="mx_cryptoEvent_buttons">
@ -180,7 +180,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
} else {
// request sent by us
title = _t("You sent a verification request");
subtitle = userLabelForEventRoom(client, request.receivingUserId, mxEvent.getRoomId()!);
subtitle = userLabelForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!);
}
if (title) {

View File

@ -165,7 +165,7 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
onClose={onClose}
member={member}
request={request}
key={request.channel.transactionId}
key={request.transactionId}
inDialog={layout === "dialog"}
phase={phase}
/>

View File

@ -67,6 +67,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
const { member, request } = this.props;
const showSAS: boolean = request.otherPartySupportsMethod(verificationMethods.SAS);
const showQR: boolean = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
const qrCodeBytes = showQR ? request.getQRCodeBytes() : null;
const brand = SdkConfig.get().brand;
const noCommonMethodError: JSX.Element | null =
@ -85,11 +86,11 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
// HACK: This is a terrible idea.
let qrBlockDialog: JSX.Element | undefined;
let sasBlockDialog: JSX.Element | undefined;
if (showQR && request.qrCodeData) {
if (!!qrCodeBytes) {
qrBlockDialog = (
<div className="mx_VerificationPanel_QRPhase_startOption">
<p>{_t("Scan this unique code")}</p>
<VerificationQRCode qrCodeData={request.qrCodeData} />
<VerificationQRCode qrCodeBytes={qrCodeBytes} />
</div>
);
}
@ -133,7 +134,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
}
let qrBlock: JSX.Element | undefined;
if (showQR && request.qrCodeData) {
if (!!qrCodeBytes) {
qrBlock = (
<div className="mx_UserInfo_container">
<h3>{_t("Verify by scanning")}</h3>
@ -144,7 +145,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
</p>
<div className="mx_VerificationPanel_qrCode">
<VerificationQRCode qrCodeData={request.qrCodeData} />
<VerificationQRCode qrCodeBytes={qrCodeBytes} />
</div>
</div>
);
@ -201,7 +202,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
};
private getDevice(): DeviceInfo | null {
const deviceId = this.props.request && this.props.request.channel.deviceId;
const deviceId = this.props.request && this.props.request.otherDeviceId;
const userId = MatrixClientPeg.get().getUserId();
if (deviceId && userId) {
return MatrixClientPeg.get().getStoredDevice(userId, deviceId);
@ -275,12 +276,12 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
if (!device) {
// This can happen if the device is logged out while we're still showing verification
// UI for it.
logger.warn("Verified device we don't know about: " + this.props.request.channel.deviceId);
logger.warn("Verified device we don't know about: " + this.props.request.otherDeviceId);
description = _t("You've successfully verified your device!");
} else {
description = _t("You've successfully verified %(deviceName)s (%(deviceId)s)!", {
deviceName: device ? device.getDisplayName() : "",
deviceId: this.props.request.channel.deviceId,
deviceId: this.props.request.otherDeviceId,
});
}
} else {

View File

@ -74,11 +74,11 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
if (request.isSelfVerification) {
const cli = MatrixClientPeg.get();
const device = request.channel.deviceId ? await cli.getDevice(request.channel.deviceId) : null;
const device = request.otherDeviceId ? await cli.getDevice(request.otherDeviceId) : null;
const ip = device?.last_seen_ip;
this.setState({
device:
(request.channel.deviceId && cli.getStoredDevice(cli.getSafeUserId(), request.channel.deviceId)) ||
(request.otherDeviceId && cli.getStoredDevice(cli.getSafeUserId(), request.otherDeviceId)) ||
undefined,
ip,
});
@ -113,10 +113,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
// no room id for to_device requests
const cli = MatrixClientPeg.get();
try {
if (request.channel.roomId) {
if (request.roomId) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: request.channel.roomId,
room_id: request.roomId,
should_peek: false,
metricsTrigger: "VerificationRequest",
});
@ -128,7 +128,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
{ phase: RightPanelPhases.EncryptionPanel, state: { verificationRequest: request, member } },
],
undefined,
request.channel.roomId,
request.roomId,
);
} else {
Modal.createDialog(
@ -164,7 +164,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
}
} else {
const userId = request.otherUserId;
const roomId = request.channel.roomId;
const roomId = request.roomId;
description = roomId ? userLabelForEventRoom(MatrixClientPeg.get(), userId, roomId) : userId;
// for legacy to_device verification requests
if (description === userId) {

View File

@ -0,0 +1,33 @@
/*
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 { cleanup, render, waitFor } from "@testing-library/react";
import React from "react";
import VerificationQRCode from "../../../../../src/components/views/elements/crypto/VerificationQRCode";
describe("<VerificationQRCode />", () => {
afterEach(() => {
cleanup();
});
it("renders a QR code", async () => {
const { container, getAllByAltText } = render(<VerificationQRCode qrCodeBytes={Buffer.from("asd")} />);
// wait for the spinner to go away
await waitFor(() => getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VerificationQRCode /> renders a QR code 1`] = `
<div>
<div
class="mx_QRCode mx_VerificationQRCode"
>
<img
alt="QR Code"
class="mx_VerificationQRCode"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADApo5rAAAAAklEQVR4AewaftIAAAPVSURBVO3BQQ4bRxAEwawG///lsiADfdqBQWpFkVZGpD8g6adB0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSevBmSfhWbXlWEk7acpKEZ7XlJAnfqi3vMkhag6Q1SFqDpDVIWg8+RFs+QRLeJQmvaMud2vIJkvCnDZLWIGkNktYgaQ2S1iBpPfgCSbhTW+6WhGe15SQJJ0m40pa7JeFObflkg6Q1SFqDpDVIWoOkNUhaD3SLtlxJwt3aot9jkLQGSWuQtAZJa5C0BknrgX6rtpwk4aQtJ0m40hb9mkHSGiStQdIaJK1B0nrwBdryf9SWT9eWv8kgaQ2S1iBpDZLWIGkNktaDD5GEb5aEK205ScJJW94lCfrXIGkNktYgaQ2S1iBpDZLWgzdry98kCSdtOUnCndqi/zZIWoOkNUhag6Q1SFqDpPXgzZJwp7acJOGTJeEVbbmShFe05U5JOGnLJxskrUHSGiStQdIaJK0Hb9aWK0k4actJEk7aciUJJ235Vm05ScKdknDSllck4Upb3mWQtAZJa5C0BklrkLQGSevBmyXhSlte0ZY7JeEVbbmShJO2vCIJV9py0pZXJOFOSfhkg6Q1SFqDpDVIWoOkNUhaD/7HknClLZ8gCSdtOWnLs5Jw0paTttypLZ9skLQGSWuQtAZJa5C0BknrwRdIwrsk4aQtd2rLK5JwpS13S8KVttwtCVfa8i6DpDVIWoOkNUhag6SV/oB+WRLepS1XknDSlpMknLTlTkl4VlveZZC0BklrkLQGSWuQtAZJ68GbJeFbteVZbTlJwrsk4V2S8K0GSWuQtAZJa5C0BklrkLQefIi2fIIkvEtbTpJwkoRnteUkCSdJeFZbXpGEP22QtAZJa5C0BklrkLQGSevBF0jCndryzdpyJQknSXhFW56VhJO2fLJB0hokrUHSGiStQdJ6oFu05UoSTtpy0pZnteUkCSdteZcknLTlTxskrUHSGiStQdIaJK1B0nqgWyThWUk4actJEq605aQtr0jClbactOUVSbjSlncZJK1B0hokrUHSGiStQdJ68AXa8q3acpKEV7TlWUk4actJW56VhFe05U8bJK1B0hokrUHSGiStQdJ68CGS8LdpyyuScKUtd0vClbbcLQlX2vIug6Q1SFqDpDVIWoOklf6ApJ8GSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtP4BohkVlic75PUAAAAASUVORK5CYII="
/>
</div>
</div>
`;

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from "react";
import { render } from "@testing-library/react";
import { render, within } from "@testing-library/react";
import { EventEmitter } from "events";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import {
@ -74,6 +74,29 @@ describe("MKeyVerificationRequest", () => {
expect(container).toHaveTextContent("You sent a verification request");
});
it("should render appropriately when the request was initiated by me and has been accepted", () => {
const event = new MatrixEvent({ type: "m.key.verification.request" });
event.verificationRequest = getMockVerificationRequest({
phase: VerificationPhase.Ready,
otherUserId: "@other:user",
});
const { container } = render(<MKeyVerificationRequest mxEvent={event} />);
expect(container).toHaveTextContent("You sent a verification request");
expect(within(container).getByRole("button")).toHaveTextContent("@other:user accepted");
});
it("should render appropriately when the request was initiated by the other user and has been accepted", () => {
const event = new MatrixEvent({ type: "m.key.verification.request" });
event.verificationRequest = getMockVerificationRequest({
phase: VerificationPhase.Ready,
initiatedByMe: false,
otherUserId: "@other:user",
});
const { container } = render(<MKeyVerificationRequest mxEvent={event} />);
expect(container).toHaveTextContent("@other:user wants to verify");
expect(within(container).getByRole("button")).toHaveTextContent("You accepted");
});
it("should render appropriately when the request was cancelled", () => {
const event = new MatrixEvent({ type: "m.key.verification.request" });
event.verificationRequest = getMockVerificationRequest({

View File

@ -14,8 +14,8 @@ 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 { act, render, waitFor } from "@testing-library/react";
import React, { ComponentProps } from "react";
import {
Phase,
VerificationRequest,
@ -31,7 +31,6 @@ import {
VerifierEvent,
VerifierEventHandlerMap,
} from "matrix-js-sdk/src/crypto-api/verification";
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";
@ -41,14 +40,59 @@ describe("<VerificationPanel />", () => {
stubClient();
});
it("should show a 'Verify by emoji' button", () => {
describe("'Ready' phase (dialog mode)", () => {
it("should show a 'Start' button", () => {
const container = renderComponent({
request: makeMockVerificationRequest(),
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.getQRCodeBytes.mockReturnValue(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.getQRCodeBytes.mockReturnValue(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<VerificationBase>;
let mockRequest: Mocked<VerificationRequest>;
@ -91,13 +135,14 @@ describe("<VerificationPanel />", () => {
});
});
function renderComponent(props: { request: VerificationRequest; phase: Phase }) {
function renderComponent(props: Partial<ComponentProps<typeof VerificationPanel>> & { request: VerificationRequest }) {
const defaultProps = {
layout: "",
member: {} as User,
onClose: () => undefined,
isRoomEncrypted: false,
inDialog: false,
phase: props.request.phase,
};
return render(<VerificationPanel {...defaultProps} {...props} />);
}
@ -105,9 +150,9 @@ function renderComponent(props: { request: VerificationRequest; phase: Phase })
function makeMockVerificationRequest(props: Partial<VerificationRequest> = {}): Mocked<VerificationRequest> {
const request = new TypedEventEmitter<VerificationRequestEvent, any>();
Object.assign(request, {
channel: {} as IVerificationChannel,
cancel: jest.fn(),
otherPartySupportsMethod: jest.fn().mockReturnValue(true),
getQRCodeBytes: jest.fn(),
...props,
});
return request as unknown as Mocked<VerificationRequest>;

View File

@ -0,0 +1,93 @@
/*
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 React, { ComponentProps } from "react";
import { Mocked } from "jest-mock";
import { act, render, RenderResult } from "@testing-library/react";
import {
VerificationRequest,
VerificationRequestEvent,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/client";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import VerificationRequestToast from "../../../../src/components/views/toasts/VerificationRequestToast";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
function renderComponent(
props: Partial<ComponentProps<typeof VerificationRequestToast>> & { request: VerificationRequest },
): RenderResult {
const propsWithDefaults = {
toastKey: "test",
...props,
};
return render(<VerificationRequestToast {...propsWithDefaults} />);
}
describe("VerificationRequestToast", () => {
let client: Mocked<MatrixClient>;
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
getStoredDevice: jest.fn(),
getDevice: jest.fn(),
});
});
it("should render a self-verification", async () => {
const otherDeviceId = "other_device";
const otherIDevice: IMyDevice = { device_id: otherDeviceId, last_seen_ip: "1.1.1.1" };
client.getDevice.mockResolvedValue(otherIDevice);
const otherDeviceInfo = new DeviceInfo(otherDeviceId);
otherDeviceInfo.unsigned = { device_display_name: "my other device" };
client.getStoredDevice.mockReturnValue(otherDeviceInfo);
const request = makeMockVerificationRequest({
isSelfVerification: true,
otherDeviceId,
});
const result = renderComponent({ request });
await act(async () => {
await flushPromises();
});
expect(result.container).toMatchSnapshot();
});
it("should render a cross-user verification", async () => {
const otherUserId = "@other:user";
const request = makeMockVerificationRequest({
isSelfVerification: false,
otherUserId,
});
const result = renderComponent({ request });
await act(async () => {
await flushPromises();
});
expect(result.container).toMatchSnapshot();
});
});
function makeMockVerificationRequest(props: Partial<VerificationRequest> = {}): Mocked<VerificationRequest> {
const request = new TypedEventEmitter<VerificationRequestEvent, any>();
Object.assign(request, {
...props,
});
return request as unknown as Mocked<VerificationRequest>;
}

View File

@ -0,0 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VerificationRequestToast should render a cross-user verification 1`] = `
<div>
<div>
<div
class="mx_Toast_description"
>
@alice:domain (@other:user)
</div>
<div
aria-live="off"
class="mx_Toast_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline"
role="button"
tabindex="0"
>
Ignore (NaN)
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Verify Session
</div>
</div>
</div>
</div>
`;
exports[`VerificationRequestToast should render a self-verification 1`] = `
<div>
<div>
<div
class="mx_Toast_description"
>
my other device
<div
class="mx_Toast_detail"
>
other_device from 1.1.1.1
</div>
</div>
<div
aria-live="off"
class="mx_Toast_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline"
role="button"
tabindex="0"
>
Ignore (NaN)
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Verify Session
</div>
</div>
</div>
</div>
`;