Provide a devtool for manually verifying other devices (#30094)
Also allows doing the same thing via a slash command.
This commit is contained in:
@@ -21,6 +21,7 @@ import { WidgetType } from "../../src/widgets/WidgetType";
|
||||
import { warnSelfDemote } from "../../src/components/views/right_panel/UserInfo";
|
||||
import dispatcher from "../../src/dispatcher/dispatcher";
|
||||
import { SettingLevel } from "../../src/settings/SettingLevel";
|
||||
import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
jest.mock("../../src/components/views/right_panel/UserInfo");
|
||||
|
||||
@@ -253,6 +254,20 @@ describe("SlashCommands", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("/verify", () => {
|
||||
it("should return usage if no args", () => {
|
||||
const command = findCommand("verify")!;
|
||||
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||
});
|
||||
|
||||
it("should show an error if device is not found", async () => {
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
const command = findCommand("verify")!;
|
||||
await command.run(client, roomId, null, "mydeviceid myfingerprint").promise;
|
||||
expect(spy).toHaveBeenCalledWith(ErrorDialog, expect.objectContaining({ title: "Verification failed" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("/addwidget", () => {
|
||||
it("should parse html iframe snippets", async () => {
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2024-2025 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 React from "react";
|
||||
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { DeviceVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { ManualDeviceKeyVerificationDialog } from "../../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
describe("ManualDeviceKeyVerificationDialog", () => {
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
function renderDialog(onFinished: (confirm: boolean) => void) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ManualDeviceKeyVerificationDialog onFinished={onFinished} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = stubClient();
|
||||
mockExistingDevices();
|
||||
});
|
||||
|
||||
it("should render correctly", () => {
|
||||
// When we render a dialog populated with data
|
||||
const { dialog } = populateDialog("XYZ", "ABCDEFGH");
|
||||
|
||||
// Then the dialog looks as expected
|
||||
expect(dialog.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call onFinished and crossSignDevice if we click Verify", async () => {
|
||||
// Given a dialog populated with correct data
|
||||
const { dialog, onFinished } = populateDialog("DEVICEID", "FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
dialog.getByRole("button", { name: "Verify session" }).click();
|
||||
|
||||
// Then crossSignDevice is called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).toHaveBeenCalledWith("DEVICEID");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if fingerprint is wrong", async () => {
|
||||
// Given a dialog populated with incorrect fingerprint
|
||||
const { dialog, onFinished } = populateDialog("DEVICEID", "WRONG_FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(
|
||||
screen.getByText(
|
||||
"the supplied fingerprint 'WRONG_FINGERPRINT' does not match the device fingerprint, 'FINGERPRINT'",
|
||||
{ exact: false },
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if device is already verified", async () => {
|
||||
// Given a dialog populated with a correct fingerprint for a verified device
|
||||
const { dialog, onFinished } = populateDialog("VERIFIED_DEVICEID", "VERIFIED_FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(screen.getByText("Failed to verify 'VERIFIED_DEVICEID': This device is already verified")).toBeVisible();
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if device is already verified and fingerprint is wrong", async () => {
|
||||
// Given a dialog populated with an incorrect fingerprint for a verified device
|
||||
const { dialog, onFinished } = populateDialog("VERIFIED_DEVICEID", "WRONG_FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(
|
||||
screen.getByText("The supplied fingerprint does not match, but the device is already verified!", {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("should not call crossSignDevice if device is not found", async () => {
|
||||
// Given a dialog populated with incorrect device ID
|
||||
const { dialog, onFinished } = populateDialog("WRONG_DEVICE_ID", "FINGERPRINT");
|
||||
|
||||
// When we click Verify session
|
||||
act(() => dialog.getByRole("button", { name: "Verify session" }).click());
|
||||
|
||||
// Then crossSignDevice is not called
|
||||
await waitFor(async () => {
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// And an error is displayed
|
||||
expect(screen.getByText("device 'WRONG_DEVICE_ID' was not found", { exact: false })).toBeVisible();
|
||||
});
|
||||
|
||||
it("should call onFinished but not crossSignDevice if we click Cancel", () => {
|
||||
// Given a dialog populated with correct data
|
||||
const { dialog, onFinished } = populateDialog("DEVICEID", "FINGERPRINT");
|
||||
|
||||
// When we click cancel
|
||||
dialog.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
// Then only onFinished is called
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(mockClient.getCrypto()?.crossSignDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function unverifiedDevice(): DeviceVerificationStatus {
|
||||
return new DeviceVerificationStatus({});
|
||||
}
|
||||
|
||||
function verifiedDevice(): DeviceVerificationStatus {
|
||||
return new DeviceVerificationStatus({
|
||||
signedByOwner: true,
|
||||
crossSigningVerified: true,
|
||||
tofu: true,
|
||||
localVerified: true,
|
||||
trustCrossSignedDevices: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up two devices: DEVICEID, which is unverified, and VERIFIED_DEVICEID, which is verified.
|
||||
*/
|
||||
function mockExistingDevices() {
|
||||
mockClient.getCrypto()!.getDeviceVerificationStatus = jest
|
||||
.fn()
|
||||
.mockImplementation(async (_userId, deviceId) =>
|
||||
deviceId === "DEVICEID" ? unverifiedDevice() : verifiedDevice(),
|
||||
);
|
||||
|
||||
mockClient.getCrypto()!.getUserDeviceInfo = jest.fn().mockImplementation(async (userIds) => {
|
||||
const userDevices = new Map();
|
||||
userDevices.set("DEVICEID", { getFingerprint: jest.fn().mockReturnValue("FINGERPRINT") });
|
||||
userDevices.set("VERIFIED_DEVICEID", { getFingerprint: jest.fn().mockReturnValue("VERIFIED_FINGERPRINT") });
|
||||
|
||||
const deviceMap = new Map();
|
||||
for (const userId of userIds) {
|
||||
deviceMap.set(userId, userDevices);
|
||||
}
|
||||
return deviceMap;
|
||||
});
|
||||
}
|
||||
|
||||
function populateDialog(deviceId: string, fingerprint: string) {
|
||||
const onFinished = jest.fn();
|
||||
const dialog = renderDialog(onFinished);
|
||||
const deviceIdBox = dialog.getByRole("textbox", { name: "Device ID" });
|
||||
const fingerprintBox = dialog.getByRole("textbox", { name: "Fingerprint (session key)" });
|
||||
fireEvent.change(deviceIdBox, { target: { value: deviceId } });
|
||||
fireEvent.change(fingerprintBox, { target: { value: fingerprint } });
|
||||
return { dialog, onFinished };
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ManualDeviceKeyVerificationDialog should render correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Verify session
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
Supply the ID and fingerprint of one of your own devices to verify it.
|
||||
</p>
|
||||
<div
|
||||
class="mx_DeviceVerifyDialog_cryptoSection"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_TextInputDialog_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Device ID"
|
||||
placeholder="Device ID"
|
||||
type="text"
|
||||
value="XYZ"
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Device ID
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_TextInputDialog_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_2"
|
||||
label="Fingerprint (session key)"
|
||||
placeholder="Fingerprint (session key)"
|
||||
type="text"
|
||||
value="ABCDEFGH"
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_2"
|
||||
>
|
||||
Fingerprint (session key)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Verify session
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
Reference in New Issue
Block a user