353 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| Copyright 2023 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.
 | |
| */
 | |
| 
 | |
| import React from "react";
 | |
| import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
 | |
| import {
 | |
|     EventType,
 | |
|     GuestAccess,
 | |
|     HistoryVisibility,
 | |
|     JoinRule,
 | |
|     MatrixEvent,
 | |
|     Room,
 | |
|     ClientEvent,
 | |
|     RoomMember,
 | |
|     MatrixError,
 | |
|     Visibility,
 | |
| } from "matrix-js-sdk/src/matrix";
 | |
| import { defer, IDeferred } from "matrix-js-sdk/src/utils";
 | |
| 
 | |
| import {
 | |
|     clearAllModals,
 | |
|     flushPromises,
 | |
|     getMockClientWithEventEmitter,
 | |
|     mockClientMethodsUser,
 | |
| } from "../../../test-utils";
 | |
| import { filterBoolean } from "../../../../src/utils/arrays";
 | |
| import JoinRuleSettings, { JoinRuleSettingsProps } from "../../../../src/components/views/settings/JoinRuleSettings";
 | |
| import { PreferredRoomVersions } from "../../../../src/utils/PreferredRoomVersions";
 | |
| import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
 | |
| import SettingsStore from "../../../../src/settings/SettingsStore";
 | |
| 
 | |
| describe("<JoinRuleSettings />", () => {
 | |
|     const userId = "@alice:server.org";
 | |
|     const client = getMockClientWithEventEmitter({
 | |
|         ...mockClientMethodsUser(userId),
 | |
|         getRoom: jest.fn(),
 | |
|         getDomain: jest.fn(),
 | |
|         getLocalAliases: jest.fn().mockReturnValue([]),
 | |
|         sendStateEvent: jest.fn(),
 | |
|         upgradeRoom: jest.fn(),
 | |
|         getProfileInfo: jest.fn(),
 | |
|         invite: jest.fn().mockResolvedValue(undefined),
 | |
|         isRoomEncrypted: jest.fn().mockReturnValue(false),
 | |
|         getRoomDirectoryVisibility: jest.fn(),
 | |
|         setRoomDirectoryVisibility: jest.fn(),
 | |
|     });
 | |
|     const roomId = "!room:server.org";
 | |
|     const newRoomId = "!roomUpgraded:server.org";
 | |
| 
 | |
|     const defaultProps = {
 | |
|         room: new Room(roomId, client, userId),
 | |
|         closeSettingsFn: jest.fn(),
 | |
|         onError: jest.fn(),
 | |
|     };
 | |
|     const getComponent = (props: Partial<JoinRuleSettingsProps> = {}) =>
 | |
|         render(<JoinRuleSettings {...defaultProps} {...props} />);
 | |
| 
 | |
|     const setRoomStateEvents = (
 | |
|         room: Room,
 | |
|         roomVersion: string,
 | |
|         joinRule?: JoinRule,
 | |
|         guestAccess?: GuestAccess,
 | |
|         history?: HistoryVisibility,
 | |
|     ): void => {
 | |
|         const events = filterBoolean<MatrixEvent>([
 | |
|             new MatrixEvent({
 | |
|                 type: EventType.RoomCreate,
 | |
|                 content: { room_version: roomVersion },
 | |
|                 sender: userId,
 | |
|                 state_key: "",
 | |
|                 room_id: room.roomId,
 | |
|             }),
 | |
|             guestAccess &&
 | |
|                 new MatrixEvent({
 | |
|                     type: EventType.RoomGuestAccess,
 | |
|                     content: { guest_access: guestAccess },
 | |
|                     sender: userId,
 | |
|                     state_key: "",
 | |
|                     room_id: room.roomId,
 | |
|                 }),
 | |
|             history &&
 | |
|                 new MatrixEvent({
 | |
|                     type: EventType.RoomHistoryVisibility,
 | |
|                     content: { history_visibility: history },
 | |
|                     sender: userId,
 | |
|                     state_key: "",
 | |
|                     room_id: room.roomId,
 | |
|                 }),
 | |
|             joinRule &&
 | |
|                 new MatrixEvent({
 | |
|                     type: EventType.RoomJoinRules,
 | |
|                     content: { join_rule: joinRule },
 | |
|                     sender: userId,
 | |
|                     state_key: "",
 | |
|                     room_id: room.roomId,
 | |
|                 }),
 | |
|         ]);
 | |
| 
 | |
|         room.currentState.setStateEvents(events);
 | |
|     };
 | |
| 
 | |
|     beforeEach(() => {
 | |
|         client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
 | |
|         client.isRoomEncrypted.mockReturnValue(false);
 | |
|         client.upgradeRoom.mockResolvedValue({ replacement_room: newRoomId });
 | |
|         client.getRoom.mockReturnValue(null);
 | |
|         jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
 | |
|     });
 | |
| 
 | |
|     type TestCase = [string, { label: string; unsupportedRoomVersion: string; preferredRoomVersion: string }];
 | |
|     const testCases: TestCase[] = [
 | |
|         [
 | |
|             JoinRule.Knock,
 | |
|             {
 | |
|                 label: "Ask to join",
 | |
|                 unsupportedRoomVersion: "6",
 | |
|                 preferredRoomVersion: PreferredRoomVersions.KnockRooms,
 | |
|             },
 | |
|         ],
 | |
|         [
 | |
|             JoinRule.Restricted,
 | |
|             {
 | |
|                 label: "Space members",
 | |
|                 unsupportedRoomVersion: "8",
 | |
|                 preferredRoomVersion: PreferredRoomVersions.RestrictedRooms,
 | |
|             },
 | |
|         ],
 | |
|     ];
 | |
| 
 | |
|     describe.each(testCases)("%s rooms", (joinRule, { label, unsupportedRoomVersion, preferredRoomVersion }) => {
 | |
|         afterEach(async () => {
 | |
|             await clearAllModals();
 | |
|         });
 | |
| 
 | |
|         describe(`when room does not support join rule ${joinRule}`, () => {
 | |
|             it(`should not show ${joinRule} room join rule when upgrade is disabled`, () => {
 | |
|                 // room that doesn't support the join rule
 | |
|                 const room = new Room(roomId, client, userId);
 | |
|                 setRoomStateEvents(room, unsupportedRoomVersion);
 | |
| 
 | |
|                 getComponent({ room: room, promptUpgrade: false });
 | |
| 
 | |
|                 expect(screen.queryByText(label)).not.toBeInTheDocument();
 | |
|             });
 | |
| 
 | |
|             it(`should show ${joinRule} room join rule when upgrade is enabled`, () => {
 | |
|                 // room that doesn't support the join rule
 | |
|                 const room = new Room(roomId, client, userId);
 | |
|                 setRoomStateEvents(room, unsupportedRoomVersion);
 | |
| 
 | |
|                 getComponent({ room: room, promptUpgrade: true });
 | |
| 
 | |
|                 expect(within(screen.getByText(label)).getByText("Upgrade required")).toBeInTheDocument();
 | |
|             });
 | |
| 
 | |
|             it(`upgrades room when changing join rule to ${joinRule}`, async () => {
 | |
|                 const deferredInvites: IDeferred<any>[] = [];
 | |
|                 // room that doesn't support the join rule
 | |
|                 const room = new Room(roomId, client, userId);
 | |
|                 const parentSpace = new Room("!parentSpace:server.org", client, userId);
 | |
|                 jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parentSpace.roomId]));
 | |
|                 setRoomStateEvents(room, unsupportedRoomVersion);
 | |
|                 const memberAlice = new RoomMember(roomId, "@alice:server.org");
 | |
|                 const memberBob = new RoomMember(roomId, "@bob:server.org");
 | |
|                 const memberCharlie = new RoomMember(roomId, "@charlie:server.org");
 | |
|                 jest.spyOn(room, "getMembersWithMembership").mockImplementation((membership) =>
 | |
|                     membership === "join" ? [memberAlice, memberBob] : [memberCharlie],
 | |
|                 );
 | |
|                 const upgradedRoom = new Room(newRoomId, client, userId);
 | |
|                 setRoomStateEvents(upgradedRoom, preferredRoomVersion);
 | |
|                 client.getRoom.mockImplementation((id) => {
 | |
|                     if (roomId === id) return room;
 | |
|                     if (parentSpace.roomId === id) return parentSpace;
 | |
|                     return null;
 | |
|                 });
 | |
| 
 | |
|                 // resolve invites by hand
 | |
|                 // flushPromises is too blunt to test reliably
 | |
|                 client.invite.mockImplementation(() => {
 | |
|                     const p = defer<{}>();
 | |
|                     deferredInvites.push(p);
 | |
|                     return p.promise;
 | |
|                 });
 | |
| 
 | |
|                 getComponent({ room: room, promptUpgrade: true });
 | |
| 
 | |
|                 fireEvent.click(screen.getByText(label));
 | |
| 
 | |
|                 const dialog = await screen.findByRole("dialog");
 | |
| 
 | |
|                 fireEvent.click(within(dialog).getByText("Upgrade"));
 | |
| 
 | |
|                 expect(client.upgradeRoom).toHaveBeenCalledWith(roomId, preferredRoomVersion);
 | |
| 
 | |
|                 expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
 | |
| 
 | |
|                 await flushPromises();
 | |
| 
 | |
|                 expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
 | |
| 
 | |
|                 // "create" our new room, have it come thru sync
 | |
|                 client.getRoom.mockImplementation((id) => {
 | |
|                     if (roomId === id) return room;
 | |
|                     if (newRoomId === id) return upgradedRoom;
 | |
|                     if (parentSpace.roomId === id) return parentSpace;
 | |
|                     return null;
 | |
|                 });
 | |
|                 client.emit(ClientEvent.Room, upgradedRoom);
 | |
| 
 | |
|                 // invite users
 | |
|                 expect(await screen.findByText("Sending invites... (0 out of 2)")).toBeInTheDocument();
 | |
|                 deferredInvites.pop()!.resolve({});
 | |
|                 expect(await screen.findByText("Sending invites... (1 out of 2)")).toBeInTheDocument();
 | |
|                 deferredInvites.pop()!.resolve({});
 | |
| 
 | |
|                 // Usually we see "Updating space..." in the UI here, but we
 | |
|                 // removed the assertion about it, because it sometimes fails,
 | |
|                 // presumably because it disappeared too quickly to be visible.
 | |
| 
 | |
|                 await flushPromises();
 | |
| 
 | |
|                 // done, modal closed
 | |
|                 await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
 | |
|             });
 | |
| 
 | |
|             it(`upgrades room with no parent spaces or members when changing join rule to ${joinRule}`, async () => {
 | |
|                 // room that doesn't support the join rule
 | |
|                 const room = new Room(roomId, client, userId);
 | |
|                 setRoomStateEvents(room, unsupportedRoomVersion);
 | |
|                 const upgradedRoom = new Room(newRoomId, client, userId);
 | |
|                 setRoomStateEvents(upgradedRoom, preferredRoomVersion);
 | |
| 
 | |
|                 getComponent({ room: room, promptUpgrade: true });
 | |
| 
 | |
|                 fireEvent.click(screen.getByText(label));
 | |
| 
 | |
|                 const dialog = await screen.findByRole("dialog");
 | |
| 
 | |
|                 fireEvent.click(within(dialog).getByText("Upgrade"));
 | |
| 
 | |
|                 expect(client.upgradeRoom).toHaveBeenCalledWith(roomId, preferredRoomVersion);
 | |
| 
 | |
|                 expect(within(dialog).getByText("Upgrading room")).toBeInTheDocument();
 | |
| 
 | |
|                 await flushPromises();
 | |
| 
 | |
|                 expect(within(dialog).getByText("Loading new room")).toBeInTheDocument();
 | |
| 
 | |
|                 // "create" our new room, have it come thru sync
 | |
|                 client.getRoom.mockImplementation((id) => {
 | |
|                     if (roomId === id) return room;
 | |
|                     if (newRoomId === id) return upgradedRoom;
 | |
|                     return null;
 | |
|                 });
 | |
|                 client.emit(ClientEvent.Room, upgradedRoom);
 | |
| 
 | |
|                 await flushPromises();
 | |
|                 await flushPromises();
 | |
| 
 | |
|                 // done, modal closed
 | |
|                 expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("knock rooms directory visibility", () => {
 | |
|         const getCheckbox = () => screen.getByRole("checkbox");
 | |
|         let room: Room;
 | |
| 
 | |
|         beforeEach(() => (room = new Room(roomId, client, userId)));
 | |
| 
 | |
|         describe("when join rule is knock", () => {
 | |
|             beforeEach(() => setRoomStateEvents(room, PreferredRoomVersions.KnockRooms, JoinRule.Knock));
 | |
| 
 | |
|             it("should set the visibility to public", async () => {
 | |
|                 jest.spyOn(client, "getRoomDirectoryVisibility").mockResolvedValue({ visibility: Visibility.Private });
 | |
|                 jest.spyOn(client, "setRoomDirectoryVisibility").mockResolvedValue({});
 | |
|                 getComponent({ room });
 | |
|                 fireEvent.click(getCheckbox());
 | |
|                 await act(async () => await flushPromises());
 | |
|                 expect(client.setRoomDirectoryVisibility).toHaveBeenCalledWith(roomId, Visibility.Public);
 | |
|                 expect(getCheckbox()).toBeChecked();
 | |
|             });
 | |
| 
 | |
|             it("should set the visibility to private", async () => {
 | |
|                 jest.spyOn(client, "getRoomDirectoryVisibility").mockResolvedValue({ visibility: Visibility.Public });
 | |
|                 jest.spyOn(client, "setRoomDirectoryVisibility").mockResolvedValue({});
 | |
|                 getComponent({ room });
 | |
|                 await act(async () => await flushPromises());
 | |
|                 fireEvent.click(getCheckbox());
 | |
|                 await act(async () => await flushPromises());
 | |
|                 expect(client.setRoomDirectoryVisibility).toHaveBeenCalledWith(roomId, Visibility.Private);
 | |
|                 expect(getCheckbox()).not.toBeChecked();
 | |
|             });
 | |
| 
 | |
|             it("should call onError if setting visibility fails", async () => {
 | |
|                 const error = new MatrixError();
 | |
|                 jest.spyOn(client, "getRoomDirectoryVisibility").mockResolvedValue({ visibility: Visibility.Private });
 | |
|                 jest.spyOn(client, "setRoomDirectoryVisibility").mockRejectedValue(error);
 | |
|                 getComponent({ room });
 | |
|                 fireEvent.click(getCheckbox());
 | |
|                 await act(async () => await flushPromises());
 | |
|                 expect(getCheckbox()).not.toBeChecked();
 | |
|                 expect(defaultProps.onError).toHaveBeenCalledWith(error);
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         describe("when the room version is unsupported and upgrade is enabled", () => {
 | |
|             it("should disable the checkbox", () => {
 | |
|                 setRoomStateEvents(room, "6", JoinRule.Invite);
 | |
|                 getComponent({ promptUpgrade: true, room });
 | |
|                 expect(getCheckbox()).toBeDisabled();
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         describe("when join rule is not knock", () => {
 | |
|             beforeEach(() => {
 | |
|                 setRoomStateEvents(room, PreferredRoomVersions.KnockRooms, JoinRule.Invite);
 | |
|                 getComponent({ room });
 | |
|             });
 | |
| 
 | |
|             it("should disable the checkbox", () => {
 | |
|                 expect(getCheckbox()).toBeDisabled();
 | |
|             });
 | |
| 
 | |
|             it("should set the visibility to private by default", () => {
 | |
|                 expect(getCheckbox()).not.toBeChecked();
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it("should not show knock room join rule", async () => {
 | |
|         jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
 | |
|         const room = new Room(newRoomId, client, userId);
 | |
|         setRoomStateEvents(room, PreferredRoomVersions.KnockRooms);
 | |
|         getComponent({ room });
 | |
|         expect(screen.queryByText("Ask to join")).not.toBeInTheDocument();
 | |
|     });
 | |
| });
 |