Confirm before inviting unknown users to a DM/room (#33171)
* InviteDialog: factor out startDmOrSendInvites Factor out the logic of calling `startDm` or `inviteUsers` to a helper function. We're going to need to call this from a second location soon, so this is useful groundwork. * Add `UnknownIdentityUsersWarningDialog` * Add unit tests * Update playwright tests * Convert if/else to switch statement * Convert helper functions to React components * Factor out "onRemove" callback * Add clarifying comment
This commit is contained in:
committed by
GitHub
parent
f4c62abbcd
commit
cd515444a8
@@ -7,12 +7,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, findByText } from "jest-matrix-react";
|
||||
import { findByText, fireEvent, render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { RoomType, type MatrixClient, MatrixError, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type MatrixClient, MatrixError, Room, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { mocked, type Mocked } from "jest-mock";
|
||||
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import InviteDialog from "../../../../../src/components/views/dialogs/InviteDialog";
|
||||
import { InviteKind } from "../../../../../src/components/views/dialogs/InviteDialogTypes";
|
||||
@@ -103,6 +104,11 @@ describe("InviteDialog", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
getCrypto: jest.fn().mockReturnValue({
|
||||
getUserVerificationStatus: jest
|
||||
.fn()
|
||||
.mockResolvedValue(new UserVerificationStatus(false, false, true, false)),
|
||||
}),
|
||||
getDomain: jest.fn().mockReturnValue(serverDomain),
|
||||
getUserId: jest.fn().mockReturnValue(bobId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(bobId),
|
||||
@@ -449,4 +455,44 @@ describe("InviteDialog", () => {
|
||||
await flushPromises();
|
||||
expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when inviting a user whose cryptographic identity we do not know", () => {
|
||||
beforeEach(() => {
|
||||
mocked(mockClient.getCrypto()!.getUserVerificationStatus).mockImplementation(async (u) => {
|
||||
return new UserVerificationStatus(false, false, false, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([InviteKind.Invite, InviteKind.Dm])("with invitekind '%s'", (kind) => {
|
||||
const goButtonName = kind == InviteKind.Invite ? "Invite" : "Go";
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<InviteDialog
|
||||
kind={kind as InviteKind.Invite | InviteKind.Dm}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should show a warning when inviting by user id", async () => {
|
||||
await enterIntoSearchField(aliceId);
|
||||
await userEvent.click(screen.getByRole("button", { name: goButtonName }));
|
||||
await screen.findByText("Confirm inviting them", { exact: false });
|
||||
|
||||
expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledTimes(1);
|
||||
expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledWith(aliceId);
|
||||
});
|
||||
|
||||
it("should show a warning when inviting by email address", async () => {
|
||||
await enterIntoSearchField("aaa@bbb");
|
||||
await userEvent.click(screen.getByRole("button", { name: goButtonName }));
|
||||
await screen.findByText("Confirm inviting them", { exact: false });
|
||||
|
||||
// We shouldn't call getUserVerificationStatus on an email address
|
||||
expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ComponentProps } from "react";
|
||||
import { render, type RenderResult } from "jest-matrix-react";
|
||||
import { getAllByRole, getAllByText, getByText } from "@testing-library/dom";
|
||||
|
||||
import UnknownIdentityUsersWarningDialog from "../../../../../../src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx";
|
||||
import { InviteKind } from "../../../../../../src/components/views/dialogs/InviteDialogTypes.ts";
|
||||
import { DirectoryMember, ThreepidMember } from "../../../../../../src/utils/direct-messages.ts";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
|
||||
|
||||
describe("UnknownIdentityUsersWarningDialog", () => {
|
||||
beforeEach(() => {
|
||||
getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should show entries for each user", () => {
|
||||
const result = renderComponent({
|
||||
users: [
|
||||
new DirectoryMember({ user_id: "@alice:example.com" }),
|
||||
new DirectoryMember({
|
||||
user_id: "@bob:example.net",
|
||||
display_name: "Bob",
|
||||
avatar_url: "mxc://example.com/abc",
|
||||
}),
|
||||
new ThreepidMember("charlie@example.com"),
|
||||
],
|
||||
});
|
||||
|
||||
const list = result.getByTestId("userlist");
|
||||
const entries = getAllByRole(list, "option");
|
||||
expect(entries).toHaveLength(3);
|
||||
|
||||
// No displayname so mxid is displayed twice
|
||||
expect(getAllByText(entries[0], "@alice:example.com")).toHaveLength(2);
|
||||
|
||||
getByText(entries[1], "Bob");
|
||||
getByText(entries[2], "charlie@example.com");
|
||||
});
|
||||
|
||||
describe("in DM mode", () => {
|
||||
const kind = InviteKind.Dm;
|
||||
|
||||
it("shows a 'Continue' button", () => {
|
||||
const onContinue = jest.fn();
|
||||
const result = renderComponent({ kind, onContinue });
|
||||
const continueButton = result.getByRole("button", { name: "Continue" });
|
||||
continueButton.click();
|
||||
expect(onContinue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a 'Cancel' button", () => {
|
||||
const onCancel = jest.fn();
|
||||
const result = renderComponent({ kind, onCancel });
|
||||
const cancelButton = result.getByRole("button", { name: "Cancel" });
|
||||
cancelButton.click();
|
||||
expect(onCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("in Invite mode", () => {
|
||||
const kind = InviteKind.Invite;
|
||||
|
||||
it("shows an 'Invite' button", () => {
|
||||
const onContinue = jest.fn();
|
||||
const result = renderComponent({ kind, onContinue });
|
||||
const continueButton = result.getByRole("button", { name: "Invite" });
|
||||
continueButton.click();
|
||||
expect(onContinue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a 'Remove' button", () => {
|
||||
const onRemove = jest.fn();
|
||||
const result = renderComponent({ kind, onRemove });
|
||||
const removeButton = result.getByRole("button", { name: "Remove" });
|
||||
removeButton.click();
|
||||
expect(onRemove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderComponent(props: Partial<ComponentProps<typeof UnknownIdentityUsersWarningDialog>>): RenderResult {
|
||||
const props1: ComponentProps<typeof UnknownIdentityUsersWarningDialog> = {
|
||||
onContinue: () => {},
|
||||
onCancel: () => {},
|
||||
onRemove: () => {},
|
||||
screenName: undefined,
|
||||
kind: InviteKind.Dm,
|
||||
users: [],
|
||||
...props,
|
||||
};
|
||||
return render(<UnknownIdentityUsersWarningDialog {...props1} />);
|
||||
}
|
||||
Reference in New Issue
Block a user