1b082a248c
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
428 lines
16 KiB
TypeScript
428 lines
16 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
|
|
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 { type MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
|
import { mocked } from "jest-mock";
|
|
import { act, waitFor } from "jest-matrix-react";
|
|
|
|
import { type Command, Commands, getCommand } from "../../src/SlashCommands";
|
|
import { createTestClient } from "../test-utils";
|
|
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
|
|
import { SdkContextClass } from "../../src/contexts/SDKContext";
|
|
import Modal, { type ComponentType, type IHandle } from "../../src/Modal";
|
|
import WidgetUtils from "../../src/utils/WidgetUtils";
|
|
import { WidgetType } from "../../src/widgets/WidgetType";
|
|
import { warnSelfDemote } from "../../src/components/views/right_panel/UserInfo";
|
|
import dispatcher from "../../src/dispatcher/dispatcher";
|
|
import QuestionDialog from "../../src/components/views/dialogs/QuestionDialog";
|
|
import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog";
|
|
import RoomUpgradeWarningDialog, {
|
|
type IFinishedOpts,
|
|
} from "../../src/components/views/dialogs/RoomUpgradeWarningDialog";
|
|
import { parseUpgradeRoomArgs } from "../../src/slash-commands/upgraderoom/parseUpgradeRoomArgs";
|
|
|
|
jest.mock("../../src/components/views/right_panel/UserInfo");
|
|
|
|
describe("SlashCommands", () => {
|
|
let client: MatrixClient;
|
|
const roomId = "!room:example.com";
|
|
let room: Room;
|
|
const localRoomId = LOCAL_ROOM_ID_PREFIX + "test";
|
|
let localRoom: LocalRoom;
|
|
let command: Command;
|
|
|
|
const findCommand = (cmd: string): Command | undefined => {
|
|
return Commands.find((command: Command) => command.command === cmd);
|
|
};
|
|
|
|
const setCurrentRoom = (): void => {
|
|
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(roomId);
|
|
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
|
|
if (rId === roomId) return room;
|
|
return null;
|
|
});
|
|
};
|
|
|
|
const setCurrentLocalRoom = (): void => {
|
|
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(localRoomId);
|
|
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
|
|
if (rId === localRoomId) return localRoom;
|
|
return null;
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
client = createTestClient();
|
|
|
|
room = new Room(roomId, client, client.getSafeUserId());
|
|
localRoom = new LocalRoom(localRoomId, client, client.getSafeUserId());
|
|
|
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId");
|
|
});
|
|
|
|
describe("/topic", () => {
|
|
it("sets topic", async () => {
|
|
const command = getCommand(roomId, "/topic pizza");
|
|
expect(command.cmd).toBeDefined();
|
|
expect(command.args).toBeDefined();
|
|
await command.cmd!.run(client, "room-id", null, command.args);
|
|
expect(client.setRoomTopic).toHaveBeenCalledWith("room-id", "pizza", undefined);
|
|
});
|
|
|
|
it("should show topic modal if no args passed", async () => {
|
|
const spy = jest.spyOn(Modal, "createDialog");
|
|
const command = getCommand(roomId, "/topic")!;
|
|
await command.cmd!.run(client, roomId, null);
|
|
expect(spy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe.each([
|
|
["myroomnick"],
|
|
["roomavatar"],
|
|
["myroomavatar"],
|
|
["topic"],
|
|
["roomname"],
|
|
["invite"],
|
|
["part"],
|
|
["remove"],
|
|
["ban"],
|
|
["unban"],
|
|
["op"],
|
|
["deop"],
|
|
["addwidget"],
|
|
["discardsession"],
|
|
["whois"],
|
|
["holdcall"],
|
|
["unholdcall"],
|
|
["converttodm"],
|
|
["converttoroom"],
|
|
])("/%s", (commandName: string) => {
|
|
beforeEach(() => {
|
|
command = findCommand(commandName)!;
|
|
});
|
|
|
|
describe("isEnabled", () => {
|
|
it("should return true for Room", () => {
|
|
setCurrentRoom();
|
|
expect(command.isEnabled(client, roomId)).toBe(true);
|
|
});
|
|
|
|
it("should return false for LocalRoom", () => {
|
|
setCurrentLocalRoom();
|
|
expect(command.isEnabled(client, roomId)).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("/upgraderoom", () => {
|
|
beforeEach(() => {
|
|
command = findCommand("upgraderoom")!;
|
|
setCurrentRoom();
|
|
});
|
|
|
|
it("should be enabled by default", () => {
|
|
expect(command.isEnabled(client, roomId)).toBe(true);
|
|
});
|
|
|
|
it("should return usage if given no args", () => {
|
|
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
|
expect(command.run(client, roomId, null, "").error).toBe(command.getUsage());
|
|
});
|
|
|
|
it("should accept arguments of a room version with no additional creators", () => {
|
|
expect(parseUpgradeRoomArgs("12")).toEqual({ targetVersion: "12" });
|
|
});
|
|
|
|
it("should accept arguments of a room version and additional creators", () => {
|
|
expect(parseUpgradeRoomArgs("13 @u:s.co")).toEqual({
|
|
targetVersion: "13",
|
|
additionalCreators: ["@u:s.co"],
|
|
});
|
|
|
|
expect(parseUpgradeRoomArgs("14 @u:s.co @v:s.co @w:z.uk")).toEqual({
|
|
targetVersion: "14",
|
|
additionalCreators: ["@u:s.co", "@v:s.co", "@w:z.uk"],
|
|
});
|
|
});
|
|
|
|
it("should upgrade the room when given valid arguments", async () => {
|
|
// Given we mock out creating dialogs and upgrading rooms
|
|
const createDialog = jest.spyOn(Modal, "createDialog");
|
|
const upgradeRoom = jest.fn().mockResolvedValue({ replacement_room: "!newroom" });
|
|
const resp: IFinishedOpts = { continue: true, invite: false };
|
|
createDialog.mockReturnValue({
|
|
finished: Promise.resolve([resp]),
|
|
close: jest.fn(),
|
|
});
|
|
client.upgradeRoom = upgradeRoom;
|
|
|
|
// When we run a room upgrade
|
|
const result = command.run(client, roomId, null, "12 @foo:bar.com @baz:qux.uk");
|
|
expect(result.promise).toBeDefined();
|
|
await result.promise;
|
|
|
|
// Then we warned the user
|
|
expect(createDialog).toHaveBeenCalledWith(
|
|
RoomUpgradeWarningDialog,
|
|
{ roomId: "!room:example.com", targetVersion: "12" },
|
|
undefined,
|
|
false,
|
|
true,
|
|
);
|
|
|
|
// And when they said yes, we called into upgradeRoom
|
|
expect(upgradeRoom).toHaveBeenCalledWith("!room:example.com", "12", ["@foo:bar.com", "@baz:qux.uk"]);
|
|
});
|
|
});
|
|
|
|
describe("/op", () => {
|
|
beforeEach(() => {
|
|
command = findCommand("op")!;
|
|
});
|
|
|
|
it("should return usage if no args", () => {
|
|
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
|
});
|
|
|
|
it("should reject with usage if given an invalid power level value", () => {
|
|
expect(command.run(client, roomId, null, "@bob:server Admin").error).toBe(command.getUsage());
|
|
});
|
|
|
|
it("should reject with usage for invalid input", () => {
|
|
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
|
|
});
|
|
|
|
it("should warn about self demotion", async () => {
|
|
setCurrentRoom();
|
|
const member = new RoomMember(roomId, client.getSafeUserId());
|
|
member.membership = KnownMembership.Join;
|
|
member.powerLevel = 100;
|
|
room.getMember = () => member;
|
|
command.run(client, roomId, null, `${client.getUserId()} 0`);
|
|
expect(warnSelfDemote).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should default to 50 if no powerlevel specified", async () => {
|
|
setCurrentRoom();
|
|
const member = new RoomMember(roomId, "@user:server");
|
|
member.membership = KnownMembership.Join;
|
|
room.getMember = () => member;
|
|
command.run(client, roomId, null, member.userId);
|
|
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, member.userId, 50);
|
|
});
|
|
});
|
|
|
|
describe("/deop", () => {
|
|
beforeEach(() => {
|
|
command = findCommand("deop")!;
|
|
});
|
|
|
|
it("should return usage if no args", () => {
|
|
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
|
});
|
|
|
|
it("should warn about self demotion", async () => {
|
|
setCurrentRoom();
|
|
const member = new RoomMember(roomId, client.getSafeUserId());
|
|
member.membership = KnownMembership.Join;
|
|
member.powerLevel = 100;
|
|
room.getMember = () => member;
|
|
command.run(client, roomId, null, client.getSafeUserId());
|
|
expect(warnSelfDemote).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should reject with usage for invalid input", () => {
|
|
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
|
|
});
|
|
});
|
|
|
|
describe("/part", () => {
|
|
it("should part room matching alias if found", async () => {
|
|
const room1 = new Room("room-id", client, client.getSafeUserId());
|
|
room1.getCanonicalAlias = jest.fn().mockReturnValue("#foo:bar");
|
|
const room2 = new Room("other-room", client, client.getSafeUserId());
|
|
room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar");
|
|
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
|
|
|
const command = getCommand(room1.roomId, "/part #foo:bar");
|
|
expect(command.cmd).toBeDefined();
|
|
expect(command.args).toBeDefined();
|
|
await command.cmd!.run(client, room1.roomId, null, command.args);
|
|
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
|
|
});
|
|
|
|
it("should part room matching alt alias if found", async () => {
|
|
const room1 = new Room("room-id", client, client.getSafeUserId());
|
|
room1.getAltAliases = jest.fn().mockReturnValue(["#foo:bar"]);
|
|
const room2 = new Room("other-room", client, client.getSafeUserId());
|
|
room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]);
|
|
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
|
|
|
const command = getCommand(room1.roomId, "/part #foo:bar");
|
|
expect(command.cmd).toBeDefined();
|
|
expect(command.args).toBeDefined();
|
|
await command.cmd!.run(client, room1.roomId, null, command.args!);
|
|
expect(client.leaveRoomChain).toHaveBeenCalledWith(room1.roomId, expect.anything());
|
|
});
|
|
});
|
|
|
|
describe.each(["rainbow", "rainbowme"])("/%s", (commandName: string) => {
|
|
const command = findCommand(commandName)!;
|
|
|
|
it("should return usage if no args", () => {
|
|
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
|
});
|
|
|
|
it("should make things rainbowy", () => {
|
|
return expect(
|
|
command.run(client, roomId, null, "this is a test message").promise,
|
|
).resolves.toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe.each(["shrug", "tableflip", "unflip", "lenny"])("/%s", (commandName: string) => {
|
|
const command = findCommand(commandName)!;
|
|
|
|
it("should match snapshot with no args", () => {
|
|
return expect(command.run(client, roomId, null).promise).resolves.toMatchSnapshot();
|
|
});
|
|
|
|
it("should match snapshot with args", () => {
|
|
return expect(
|
|
command.run(client, roomId, null, "this is a test message").promise,
|
|
).resolves.toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
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 attempt manual verification after confirmation", async () => {
|
|
// Given we say yes to prompt
|
|
const spy = jest.spyOn(Modal, "createDialog");
|
|
spy.mockReturnValue({ finished: Promise.resolve([true]) } as unknown as IHandle<ComponentType>);
|
|
|
|
// When we run the command
|
|
const command = findCommand("verify")!;
|
|
await act(() => command.run(client, roomId, null, "mydeviceid myfingerprint"));
|
|
|
|
// Then the prompt is displayed
|
|
expect(spy).toHaveBeenCalledWith(
|
|
QuestionDialog,
|
|
expect.objectContaining({ title: "Caution: manual device verification" }),
|
|
);
|
|
|
|
// And then we attempt the verification
|
|
await waitFor(() =>
|
|
expect(spy).toHaveBeenCalledWith(
|
|
ErrorDialog,
|
|
expect.objectContaining({ title: "Verification failed" }),
|
|
),
|
|
);
|
|
});
|
|
|
|
it("should not do manual verification if cancelled", async () => {
|
|
// Given we say no to prompt
|
|
const spy = jest.spyOn(Modal, "createDialog");
|
|
spy.mockReturnValue({ finished: Promise.resolve([false]) } as unknown as IHandle<ComponentType>);
|
|
|
|
// When we run the command
|
|
const command = findCommand("verify")!;
|
|
command.run(client, roomId, null, "mydeviceid myfingerprint");
|
|
|
|
// Then the prompt is displayed
|
|
expect(spy).toHaveBeenCalledWith(
|
|
QuestionDialog,
|
|
expect.objectContaining({ title: "Caution: manual device verification" }),
|
|
);
|
|
|
|
// But nothing else happens
|
|
expect(spy).not.toHaveBeenCalledWith(ErrorDialog, expect.anything());
|
|
});
|
|
});
|
|
|
|
describe("/addwidget", () => {
|
|
it("should parse html iframe snippets", async () => {
|
|
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
|
const spy = jest.spyOn(WidgetUtils, "setRoomWidget");
|
|
const command = findCommand("addwidget")!;
|
|
await command.run(client, roomId, null, '<iframe src="https://element.io"></iframe>');
|
|
expect(spy).toHaveBeenCalledWith(
|
|
client,
|
|
roomId,
|
|
expect.any(String),
|
|
WidgetType.CUSTOM,
|
|
"https://element.io",
|
|
"Custom",
|
|
{},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("/join", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(dispatcher, "dispatch");
|
|
command = findCommand(KnownMembership.Join)!;
|
|
});
|
|
|
|
it("should return usage if no args", () => {
|
|
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
|
});
|
|
|
|
it("should handle matrix.org permalinks", () => {
|
|
command.run(client, roomId, null, "https://matrix.to/#/!roomId:server/$eventId");
|
|
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
action: "view_room",
|
|
room_id: "!roomId:server",
|
|
event_id: "$eventId",
|
|
highlighted: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should handle room aliases", () => {
|
|
command.run(client, roomId, null, "#test:server");
|
|
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
action: "view_room",
|
|
room_alias: "#test:server",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should handle room aliases with no server component", () => {
|
|
command.run(client, roomId, null, "#test");
|
|
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
action: "view_room",
|
|
room_alias: `#test:${client.getDomain()}`,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should handle room IDs and via servers", () => {
|
|
command.run(client, roomId, null, "!foo:bar serv1.com serv2.com");
|
|
expect(dispatcher.dispatch).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
action: "view_room",
|
|
room_id: "!foo:bar",
|
|
via_servers: ["serv1.com", "serv2.com"],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|