element-web/test/components/views/settings/tabs/room/SecurityRoomSettingsTab-test.tsx
Kerry 9f580a8680
Disable encryption toggle in room settings when force disabled (#11122)
* force disable encryption on room creation

* test allowChangingEncryption

* move into utils/room directory

* tests

* unit test CreateRoomDialog

* remove debug

* wait for constructor promises to settle

* test case for force_disable

* comment

* set forced value after resolving checkUserIsAllowedToChangeEncryption

* tidy and comments

* use label text in test

* disable encryption switch in room settings when force disabled
2023-06-21 23:17:30 +00:00

448 lines
17 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 { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { EventType, GuestAccess, HistoryVisibility, JoinRule, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import SecurityRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/SecurityRoomSettingsTab";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import {
clearAllModals,
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsUser,
} from "../../../../../test-utils";
import { filterBoolean } from "../../../../../../src/utils/arrays";
describe("<SecurityRoomSettingsTab />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
isRoomEncrypted: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
sendStateEvent: jest.fn(),
getClientWellKnown: jest.fn(),
});
const roomId = "!room:server.org";
const getComponent = (room: Room, closeSettingsFn = jest.fn()) =>
render(<SecurityRoomSettingsTab room={room} closeSettingsFn={closeSettingsFn} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
),
});
const setRoomStateEvents = (
room: Room,
joinRule?: JoinRule,
guestAccess?: GuestAccess,
history?: HistoryVisibility,
): void => {
const events = filterBoolean<MatrixEvent>([
new MatrixEvent({
type: EventType.RoomCreate,
content: { version: "test" },
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(async () => {
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
client.isRoomEncrypted.mockReturnValue(false);
client.getClientWellKnown.mockReturnValue(undefined);
jest.spyOn(SettingsStore, "getValue").mockRestore();
await clearAllModals();
});
describe("join rule", () => {
it("warns when trying to make an encrypted room public", async () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
const modal = await screen.findByRole("dialog");
expect(modal).toMatchSnapshot();
fireEvent.click(screen.getByText("Cancel"));
// join rule not updated
expect(screen.getByLabelText("Private (invite only)").hasAttribute("checked")).toBeTruthy();
});
it("updates join rule", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
await flushPromises();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomJoinRules,
{
join_rule: JoinRule.Public,
},
"",
);
});
it("handles error when updating join rule fails", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
await flushPromises();
const dialog = await screen.findByRole("dialog");
expect(dialog).toMatchSnapshot();
fireEvent.click(within(dialog).getByText("OK"));
});
it("displays advanced section toggle when join rule is public", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
expect(screen.getByText("Show advanced")).toBeInTheDocument();
});
it("does not display advanced section toggle when join rule is not public", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
expect(screen.queryByText("Show advanced")).not.toBeInTheDocument();
});
});
describe("guest access", () => {
it("uses forbidden by default when room has no guest access event", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("false");
});
it("updates guest access on toggle", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
fireEvent.click(screen.getByLabelText("Enable guest access"));
// toggle set immediately
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("true");
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomGuestAccess,
{ guest_access: GuestAccess.CanJoin },
"",
);
});
it("logs error and resets state when updating guest access fails", async () => {
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public, GuestAccess.CanJoin);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
fireEvent.click(screen.getByLabelText("Enable guest access"));
// toggle set immediately
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("false");
await flushPromises();
expect(client.sendStateEvent).toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith("oups");
// toggle reset to old value
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("true");
});
});
describe("history visibility", () => {
it("does not render section when RoomHistorySettings feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.queryByText("Who can read history")).not.toBeInTheDocument();
});
it("uses shared as default history visibility when no state event found", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByText("Who can read history?").parentElement).toMatchSnapshot();
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
});
it("does not render world readable option when room is encrypted", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.queryByDisplayValue(HistoryVisibility.WorldReadable)).not.toBeInTheDocument();
});
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument();
});
it("updates history visibility", () => {
const room = new Room(roomId, client, userId);
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: HistoryVisibility.Invited,
},
"",
);
});
it("handles error when updating history visibility", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
await flushPromises();
// reset to before updated value
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
expect(logger.error).toHaveBeenCalledWith("oups");
});
});
describe("encryption", () => {
it("displays encryption as enabled", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).toBeChecked();
// can't disable encryption once enabled
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
});
it("asks users to confirm when setting room to encrypted", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
fireEvent.click(screen.getByLabelText("Encrypted"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByText("Cancel"));
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
});
it("enables encryption after confirmation", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
fireEvent.click(screen.getByLabelText("Encrypted"));
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText("Enable encryption?")).toBeInTheDocument();
fireEvent.click(within(dialog).getByText("OK"));
await waitFor(() =>
expect(client.sendStateEvent).toHaveBeenCalledWith(room.roomId, EventType.RoomEncryption, {
algorithm: "m.megolm.v1.aes-sha2",
}),
);
});
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument();
});
it("updates history visibility", () => {
const room = new Room(roomId, client, userId);
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: HistoryVisibility.Invited,
},
"",
);
});
it("handles error when updating history visibility", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
await flushPromises();
// reset to before updated value
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
expect(logger.error).toHaveBeenCalledWith("oups");
});
describe("when encryption is force disabled by e2ee well-known config", () => {
beforeEach(() => {
client.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
force_disable: true,
},
});
});
it("displays encrypted rooms as encrypted", () => {
// rooms that are already encrypted still show encrypted
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).toBeChecked();
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
});
it("displays unencrypted rooms with toggle disabled", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(false);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument();
expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument();
});
});
});
});