2022-05-05 09:02:48 +02:00
|
|
|
/*
|
|
|
|
Copyright 2022 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.
|
|
|
|
*/
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
import { mocked } from "jest-mock";
|
2023-10-25 12:08:10 +02:00
|
|
|
import { EventType, MatrixClient, MatrixError, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
2022-05-05 09:02:48 +02:00
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
2023-02-28 11:31:48 +01:00
|
|
|
import Modal, { ComponentType, ComponentProps } from "../../src/Modal";
|
2022-12-12 12:24:14 +01:00
|
|
|
import SettingsStore from "../../src/settings/SettingsStore";
|
|
|
|
import MultiInviter, { CompletionStates } from "../../src/utils/MultiInviter";
|
|
|
|
import * as TestUtilsMatrix from "../test-utils";
|
2023-02-28 11:31:48 +01:00
|
|
|
import AskInviteAnywayDialog from "../../src/components/views/dialogs/AskInviteAnywayDialog";
|
2023-07-14 15:26:02 +02:00
|
|
|
import ConfirmUserActionDialog from "../../src/components/views/dialogs/ConfirmUserActionDialog";
|
2022-05-05 09:02:48 +02:00
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
const ROOMID = "!room:server";
|
2022-05-05 09:02:48 +02:00
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
const MXID1 = "@user1:server";
|
|
|
|
const MXID2 = "@user2:server";
|
|
|
|
const MXID3 = "@user3:server";
|
2022-05-05 09:02:48 +02:00
|
|
|
|
2023-02-13 12:39:16 +01:00
|
|
|
const MXID_PROFILE_STATES: Record<string, Promise<any>> = {
|
2022-05-05 09:02:48 +02:00
|
|
|
[MXID1]: Promise.resolve({}),
|
2023-07-05 12:53:22 +02:00
|
|
|
[MXID2]: Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN" })),
|
|
|
|
[MXID3]: Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
2022-05-05 09:02:48 +02:00
|
|
|
};
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
jest.mock("../../src/Modal", () => ({
|
2022-06-14 18:51:51 +02:00
|
|
|
createDialog: jest.fn(),
|
2022-05-05 09:02:48 +02:00
|
|
|
}));
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
jest.mock("../../src/settings/SettingsStore", () => ({
|
2022-05-05 09:02:48 +02:00
|
|
|
getValue: jest.fn(),
|
2022-11-10 09:38:48 +01:00
|
|
|
monitorSetting: jest.fn(),
|
2022-12-06 10:59:17 +01:00
|
|
|
watchSetting: jest.fn(),
|
2022-05-05 09:02:48 +02:00
|
|
|
}));
|
|
|
|
|
|
|
|
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
|
|
|
mocked(SettingsStore.getValue).mockImplementation(
|
2023-02-13 18:01:43 +01:00
|
|
|
(settingName: string, roomId: string, _excludeDefault = false): any => {
|
2022-12-12 12:24:14 +01:00
|
|
|
if (settingName === "promptBeforeInviteUnknownUsers" && roomId === ROOMID) {
|
2022-05-05 09:02:48 +02:00
|
|
|
return value;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
const mockCreateTrackedDialog = (callbackName: "onInviteAnyways" | "onGiveUp") => {
|
2023-02-28 11:31:48 +01:00
|
|
|
mocked(Modal.createDialog).mockImplementation(
|
|
|
|
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
|
|
|
(props as ComponentProps<typeof AskInviteAnywayDialog>)[callbackName]();
|
|
|
|
},
|
|
|
|
);
|
2022-05-05 09:02:48 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const expectAllInvitedResult = (result: CompletionStates) => {
|
|
|
|
expect(result).toEqual({
|
2022-12-12 12:24:14 +01:00
|
|
|
[MXID1]: "invited",
|
|
|
|
[MXID2]: "invited",
|
|
|
|
[MXID3]: "invited",
|
2022-05-05 09:02:48 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
describe("MultiInviter", () => {
|
2022-05-05 09:02:48 +02:00
|
|
|
let client: jest.Mocked<MatrixClient>;
|
|
|
|
let inviter: MultiInviter;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
jest.resetAllMocks();
|
|
|
|
|
|
|
|
TestUtilsMatrix.stubClient();
|
2023-06-05 19:12:23 +02:00
|
|
|
client = MatrixClientPeg.safeGet() as jest.Mocked<MatrixClient>;
|
2022-05-05 09:02:48 +02: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 15:26:02 +02:00
|
|
|
client.unban = jest.fn();
|
2022-05-05 09:02:48 +02:00
|
|
|
|
2023-05-23 17:24:12 +02:00
|
|
|
inviter = new MultiInviter(client, ROOMID);
|
2022-05-05 09:02:48 +02:00
|
|
|
});
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
describe("invite", () => {
|
|
|
|
describe("with promptBeforeInviteUnknownUsers = false", () => {
|
2022-05-05 09:02:48 +02:00
|
|
|
beforeEach(() => mockPromptBeforeInviteUnknownUsers(false));
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
it("should invite all users", async () => {
|
2022-05-05 09:02:48 +02:00
|
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(3);
|
2022-10-12 19:59:07 +02: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 09:02:48 +02:00
|
|
|
|
|
|
|
expectAllInvitedResult(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
describe("with promptBeforeInviteUnknownUsers = true and", () => {
|
2022-05-05 09:02:48 +02:00
|
|
|
beforeEach(() => mockPromptBeforeInviteUnknownUsers(true));
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
describe("confirming the unknown user dialog", () => {
|
|
|
|
beforeEach(() => mockCreateTrackedDialog("onInviteAnyways"));
|
2022-05-05 09:02:48 +02:00
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
it("should invite all users", async () => {
|
2022-05-05 09:02:48 +02:00
|
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(3);
|
2022-10-12 19:59:07 +02: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 09:02:48 +02:00
|
|
|
|
|
|
|
expectAllInvitedResult(result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
describe("declining the unknown user dialog", () => {
|
|
|
|
beforeEach(() => mockCreateTrackedDialog("onGiveUp"));
|
2022-05-05 09:02:48 +02:00
|
|
|
|
2022-12-12 12:24:14 +01:00
|
|
|
it("should only invite existing users", async () => {
|
2022-05-05 09:02:48 +02:00
|
|
|
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
|
|
|
|
|
|
|
expect(client.invite).toHaveBeenCalledTimes(1);
|
2022-10-12 19:59:07 +02:00
|
|
|
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
2022-05-05 09:02:48 +02: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 13:04:13 +02: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 15:26:02 +02: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());
|
|
|
|
ourMember.membership = "join";
|
|
|
|
ourMember.powerLevel = 100;
|
|
|
|
const member = new RoomMember(ROOMID, MXID1);
|
|
|
|
member.membership = "ban";
|
|
|
|
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 12:08:10 +02: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 09:02:48 +02:00
|
|
|
});
|
|
|
|
});
|