2022-05-05 15:02:48 +08:00
|
|
|
/*
|
2024-09-09 21:57:16 +08:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2022-05-05 15:02:48 +08:00
|
|
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
|
|
|
2024-09-09 21:57:16 +08:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2022-05-05 15:02:48 +08:00
|
|
|
*/
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
import { mocked } from "jest-mock";
|
2024-03-18 22:40:52 +08:00
|
|
|
import { EventType, MatrixClient, MatrixError, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
|
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
2022-05-05 15:02:48 +08:00
|
|
|
|
2024-10-15 21:57:26 +08:00
|
|
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
|
|
|
import Modal, { ComponentType, ComponentProps } from "../../../src/Modal";
|
|
|
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
|
|
|
import MultiInviter, { CompletionStates } from "../../../src/utils/MultiInviter";
|
|
|
|
import * as TestUtilsMatrix from "../../test-utils";
|
|
|
|
import AskInviteAnywayDialog from "../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
|
|
|
import ConfirmUserActionDialog from "../../../src/components/views/dialogs/ConfirmUserActionDialog";
|
2022-05-05 15:02:48 +08:00
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
const ROOMID = "!room:server";
|
2022-05-05 15:02:48 +08:00
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
const MXID1 = "@user1:server";
|
|
|
|
const MXID2 = "@user2:server";
|
|
|
|
const MXID3 = "@user3:server";
|
2022-05-05 15:02:48 +08:00
|
|
|
|
2023-02-13 19:39:16 +08:00
|
|
|
const MXID_PROFILE_STATES: Record<string, Promise<any>> = {
|
2022-05-05 15:02:48 +08:00
|
|
|
[MXID1]: Promise.resolve({}),
|
2023-07-05 18:53:22 +08:00
|
|
|
[MXID2]: Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN" })),
|
|
|
|
[MXID3]: Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
2022-05-05 15:02:48 +08:00
|
|
|
};
|
|
|
|
|
2024-10-15 21:57:26 +08:00
|
|
|
jest.mock("../../../src/Modal", () => ({
|
2022-06-15 00:51:51 +08:00
|
|
|
createDialog: jest.fn(),
|
2022-05-05 15:02:48 +08:00
|
|
|
}));
|
|
|
|
|
2024-10-15 21:57:26 +08:00
|
|
|
jest.mock("../../../src/settings/SettingsStore", () => ({
|
2022-05-05 15:02:48 +08:00
|
|
|
getValue: jest.fn(),
|
2022-11-10 16:38:48 +08:00
|
|
|
monitorSetting: jest.fn(),
|
2022-12-06 17:59:17 +08:00
|
|
|
watchSetting: jest.fn(),
|
2022-05-05 15:02:48 +08:00
|
|
|
}));
|
|
|
|
|
|
|
|
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
|
|
|
mocked(SettingsStore.getValue).mockImplementation(
|
2023-02-14 01:01:43 +08:00
|
|
|
(settingName: string, roomId: string, _excludeDefault = false): any => {
|
2022-12-12 19:24:14 +08:00
|
|
|
if (settingName === "promptBeforeInviteUnknownUsers" && roomId === ROOMID) {
|
2022-05-05 15:02:48 +08:00
|
|
|
return value;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
const mockCreateTrackedDialog = (callbackName: "onInviteAnyways" | "onGiveUp") => {
|
2023-02-28 18:31:48 +08:00
|
|
|
mocked(Modal.createDialog).mockImplementation(
|
|
|
|
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
|
|
|
(props as ComponentProps<typeof AskInviteAnywayDialog>)[callbackName]();
|
|
|
|
},
|
|
|
|
);
|
2022-05-05 15:02:48 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const expectAllInvitedResult = (result: CompletionStates) => {
|
|
|
|
expect(result).toEqual({
|
2022-12-12 19:24:14 +08:00
|
|
|
[MXID1]: "invited",
|
|
|
|
[MXID2]: "invited",
|
|
|
|
[MXID3]: "invited",
|
2022-05-05 15:02:48 +08:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
describe("MultiInviter", () => {
|
2022-05-05 15:02:48 +08:00
|
|
|
let client: jest.Mocked<MatrixClient>;
|
|
|
|
let inviter: MultiInviter;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
jest.resetAllMocks();
|
|
|
|
|
|
|
|
TestUtilsMatrix.stubClient();
|
2023-06-06 01:12:23 +08:00
|
|
|
client = MatrixClientPeg.safeGet() as jest.Mocked<MatrixClient>;
|
2022-05-05 15:02:48 +08:00
|
|
|
|
|
|
|
client.invite = jest.fn();
|
|
|
|
client.invite.mockResolvedValue({});
|
|
|
|
|
|
|
|
client.getProfileInfo = jest.fn();
|
|
|
|
client.getProfileInfo.mockImplementation((userId: string) => {
|
|
|
|
return MXID_PROFILE_STATES[userId] || Promise.reject();
|
|
|
|
});
|
2023-07-14 21:26:02 +08:00
|
|
|
client.unban = jest.fn();
|
2022-05-05 15:02:48 +08:00
|
|
|
|
2023-05-23 23:24:12 +08:00
|
|
|
inviter = new MultiInviter(client, ROOMID);
|
2022-05-05 15:02:48 +08:00
|
|
|
});
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
describe("invite", () => {
|
|
|
|
describe("with promptBeforeInviteUnknownUsers = false", () => {
|
2022-05-05 15:02:48 +08:00
|
|
|
beforeEach(() => mockPromptBeforeInviteUnknownUsers(false));
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
it("should invite all users", async () => {
|
2022-05-05 15:02:48 +08:00
|
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(3);
|
2022-10-13 01:59:07 +08:00
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
|
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
|
2022-05-05 15:02:48 +08:00
|
|
|
|
|
|
|
expectAllInvitedResult(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
describe("with promptBeforeInviteUnknownUsers = true and", () => {
|
2022-05-05 15:02:48 +08:00
|
|
|
beforeEach(() => mockPromptBeforeInviteUnknownUsers(true));
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
describe("confirming the unknown user dialog", () => {
|
|
|
|
beforeEach(() => mockCreateTrackedDialog("onInviteAnyways"));
|
2022-05-05 15:02:48 +08:00
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
it("should invite all users", async () => {
|
2022-05-05 15:02:48 +08:00
|
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(3);
|
2022-10-13 01:59:07 +08:00
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
|
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
|
2022-05-05 15:02:48 +08:00
|
|
|
|
|
|
|
expectAllInvitedResult(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
describe("declining the unknown user dialog", () => {
|
|
|
|
beforeEach(() => mockCreateTrackedDialog("onGiveUp"));
|
2022-05-05 15:02:48 +08:00
|
|
|
|
2022-12-12 19:24:14 +08:00
|
|
|
it("should only invite existing users", async () => {
|
2022-05-05 15:02:48 +08:00
|
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(1);
|
2022-10-13 01:59:07 +08:00
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
2022-05-05 15:02:48 +08:00
|
|
|
|
|
|
|
// The resolved state is 'invited' for all users.
|
|
|
|
// With the above client expectations, the test ensures that only the first user is invited.
|
|
|
|
expectAllInvitedResult(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-05-05 19:04:13 +08:00
|
|
|
|
|
|
|
it("should show sensible error when attempting 3pid invite with no identity server", async () => {
|
|
|
|
client.inviteByEmail = jest.fn().mockRejectedValueOnce(
|
|
|
|
new MatrixError({
|
|
|
|
errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM",
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
await inviter.invite(["foo@bar.com"]);
|
|
|
|
expect(inviter.getErrorText("foo@bar.com")).toMatchInlineSnapshot(
|
|
|
|
`"Cannot invite user by email without an identity server. You can connect to one under "Settings"."`,
|
|
|
|
);
|
|
|
|
});
|
2023-07-14 21:26:02 +08:00
|
|
|
|
|
|
|
it("should ask if user wants to unban user if they have permission", async () => {
|
|
|
|
mocked(Modal.createDialog).mockImplementation(
|
|
|
|
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
|
|
|
// We stub out the modal with an immediate affirmative (proceed) return
|
|
|
|
return { finished: Promise.resolve([true]) };
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
|
|
|
mocked(client.getRoom).mockReturnValue(room);
|
|
|
|
const ourMember = new RoomMember(ROOMID, client.getSafeUserId());
|
2024-03-12 22:52:54 +08:00
|
|
|
ourMember.membership = KnownMembership.Join;
|
2023-07-14 21:26:02 +08:00
|
|
|
ourMember.powerLevel = 100;
|
|
|
|
const member = new RoomMember(ROOMID, MXID1);
|
2024-03-12 22:52:54 +08:00
|
|
|
member.membership = KnownMembership.Ban;
|
2023-07-14 21:26:02 +08:00
|
|
|
member.powerLevel = 0;
|
|
|
|
room.getMember = (userId: string) => {
|
|
|
|
if (userId === client.getSafeUserId()) return ourMember;
|
|
|
|
if (userId === MXID1) return member;
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
await inviter.invite([MXID1]);
|
|
|
|
expect(Modal.createDialog).toHaveBeenCalledWith(ConfirmUserActionDialog, {
|
|
|
|
member,
|
|
|
|
title: "User cannot be invited until they are unbanned",
|
|
|
|
action: "Unban",
|
|
|
|
});
|
|
|
|
expect(client.unban).toHaveBeenCalledWith(ROOMID, MXID1);
|
|
|
|
});
|
2023-10-25 18:08:10 +08:00
|
|
|
|
|
|
|
it("should show sensible error when attempting to invite over federation with m.federate=false", async () => {
|
|
|
|
mocked(client.invite).mockRejectedValueOnce(
|
|
|
|
new MatrixError({
|
|
|
|
errcode: "M_FORBIDDEN",
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
|
|
|
room.currentState.setStateEvents([
|
|
|
|
new MatrixEvent({
|
|
|
|
type: EventType.RoomCreate,
|
|
|
|
state_key: "",
|
|
|
|
content: {
|
|
|
|
"m.federate": false,
|
|
|
|
},
|
|
|
|
room_id: ROOMID,
|
|
|
|
}),
|
|
|
|
]);
|
|
|
|
mocked(client.getRoom).mockReturnValue(room);
|
|
|
|
|
|
|
|
await inviter.invite(["@user:other_server"]);
|
|
|
|
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
|
|
|
`"This room is unfederated. You cannot invite people from external servers."`,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should show sensible error when attempting to invite over federation with m.federate=false to space", async () => {
|
|
|
|
mocked(client.invite).mockRejectedValueOnce(
|
|
|
|
new MatrixError({
|
|
|
|
errcode: "M_FORBIDDEN",
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
|
|
|
room.currentState.setStateEvents([
|
|
|
|
new MatrixEvent({
|
|
|
|
type: EventType.RoomCreate,
|
|
|
|
state_key: "",
|
|
|
|
content: {
|
|
|
|
"m.federate": false,
|
|
|
|
"type": "m.space",
|
|
|
|
},
|
|
|
|
room_id: ROOMID,
|
|
|
|
}),
|
|
|
|
]);
|
|
|
|
mocked(client.getRoom).mockReturnValue(room);
|
|
|
|
|
|
|
|
await inviter.invite(["@user:other_server"]);
|
|
|
|
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
|
|
|
`"This space is unfederated. You cannot invite people from external servers."`,
|
|
|
|
);
|
|
|
|
});
|
2022-05-05 15:02:48 +08:00
|
|
|
});
|
|
|
|
});
|