* Use EditInPlace for identity server picker. * Update test * Add a test for setting an ID server. * fix tests * Reformat other :not sections * forgot a comma * Update Apperance settings to use toggle switches. * Remove unused checkbox setting. * Remove unused import. * Update tests * lint * update apperance screenshot * Begin replacing settings * Refactor RoomPublishSetting * Remove LabelledToggleSwitch * Refactor SettingsFlag to use SettingsToggleInput * Refactor CreateRoomDialog to use SettingsToggleInput * Refactor DeclineAndBlockInviteDialog to use SettingsToggleInput * Update DevtoolsDialog * Refactor ReportRoomDialog to use SettingsToggle * Update RoomUpgradeWarningDialog to use SettingsToggleInput * Update WidgetCapabilitiesPromptDialog to use SettingsToggleInput * Update trivial switchovers * Update Notifications settings to use SettingsFlag where possible * Update RoomPublishSetting and SpaceSettingVisibilityTab to use SettingsToggleInput with a warning * revert changes to field * Updated screenshots * Prevent accidental submits * Replace test ID tests * Create new snapshot tests * Add screenshot test for DeclineAndBlockDialog * Add screenshot for create room dialog. * Add devtools test * Add upgrade rooms test * Add widget capabilites prompt test * Fix spec * Add a test for the live location sharing prompt. * fix copyright * Add tests for notification settings * Add tests for user security tab. * Add test for room security tab. * Add test for video settings tab. * remove .only * Test creating a video room * Mask the IM name in the header. * Add spaces vis tab test. * Fixup unit tests to check correct attributes. * Various fixes to components for tests. * lint * Update compound * update setting names * Cleanup tests prettier Updates some more playwright tests Update more snapshots Update switch more fixes drop .only last screenshot round fix video room flake Remove console.logs Remove roomId from devtools view. lint final screenshot * Add playwright tests * import pages/ remove duplicate create-room * Update screenshots * Fix accessibility for devtools * Disable region test * Fixup headers * remove extra test * Fix permissions dialog * fixup tests * update snapshot * Update jest tests * Clear up playwright tests * update widget screenshot * Fix wrong snaps from using wrong compound version * Revert mistaken s/checkbox/switch/ * lint lint * Update headings * fix snap * remove unused * update snapshot * update tab screenshot * Update snapshots * Fix margins * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
515 lines
20 KiB
TypeScript
515 lines
20 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2023 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 React from "react";
|
|
import { fireEvent, render, screen, waitFor, within } from "jest-matrix-react";
|
|
import { EventType, GuestAccess, HistoryVisibility, JoinRule, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
|
import { mocked } from "jest-mock";
|
|
import { type RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
|
|
|
|
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, stubClient } from "../../../../../../test-utils";
|
|
import { filterBoolean } from "../../../../../../../src/utils/arrays";
|
|
|
|
describe("<SecurityRoomSettingsTab />", () => {
|
|
const userId = "@alice:server.org";
|
|
const client = mocked(stubClient());
|
|
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" });
|
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(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);
|
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
|
setRoomStateEvents(room, JoinRule.Invite);
|
|
|
|
getComponent(room);
|
|
|
|
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
|
|
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 changing room to private with world_readable history visiblity", async () => {
|
|
const room = new Room(roomId, client, userId);
|
|
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.WorldReadable);
|
|
|
|
getComponent(room);
|
|
|
|
fireEvent.click(screen.getByLabelText("Private (invite only)"));
|
|
|
|
await flushPromises();
|
|
|
|
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
|
room.roomId,
|
|
EventType.RoomHistoryVisibility,
|
|
{
|
|
history_visibility: HistoryVisibility.Shared,
|
|
},
|
|
"",
|
|
);
|
|
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
|
room.roomId,
|
|
EventType.RoomJoinRules,
|
|
{
|
|
join_rule: JoinRule.Invite,
|
|
},
|
|
"",
|
|
);
|
|
});
|
|
|
|
it("doesn't change room to private when user lacks permissions for history visibility", async () => {
|
|
const room = new Room(roomId, client, userId);
|
|
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.WorldReadable);
|
|
room.currentState.setStateEvents([
|
|
new MatrixEvent({
|
|
type: EventType.RoomPowerLevels,
|
|
content: {
|
|
users: { [userId]: 50 },
|
|
state_default: 50,
|
|
events: {
|
|
[EventType.RoomJoinRules]: 50,
|
|
[EventType.RoomHistoryVisibility]: 100,
|
|
},
|
|
} as RoomPowerLevelsEventContent,
|
|
sender: userId,
|
|
state_key: "",
|
|
room_id: room.roomId,
|
|
}),
|
|
]);
|
|
|
|
getComponent(room);
|
|
fireEvent.click(screen.getByLabelText("Private (invite only)"));
|
|
await flushPromises();
|
|
// Ensure we don't make any changes
|
|
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("doesn't change room to private when history visibility change fails", async () => {
|
|
client.sendStateEvent.mockRejectedValue("Failed");
|
|
const room = new Room(roomId, client, userId);
|
|
setRoomStateEvents(room, JoinRule.Public, undefined, HistoryVisibility.WorldReadable);
|
|
|
|
getComponent(room);
|
|
fireEvent.click(screen.getByLabelText("Private (invite only)"));
|
|
await flushPromises();
|
|
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
|
room.roomId,
|
|
EventType.RoomHistoryVisibility,
|
|
{
|
|
history_visibility: HistoryVisibility.Shared,
|
|
},
|
|
"",
|
|
);
|
|
// Ensure we don't make any changes
|
|
expect(client.sendStateEvent).not.toHaveBeenCalledWith(
|
|
room.roomId,
|
|
EventType.RoomJoinRules,
|
|
{
|
|
join_rule: JoinRule.Invite,
|
|
},
|
|
"",
|
|
);
|
|
});
|
|
|
|
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")).not.toBeChecked();
|
|
});
|
|
|
|
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")).toBeChecked();
|
|
|
|
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")).not.toBeChecked();
|
|
|
|
await flushPromises();
|
|
expect(client.sendStateEvent).toHaveBeenCalled();
|
|
expect(logger.error).toHaveBeenCalledWith("oups");
|
|
|
|
// toggle reset to old value
|
|
expect(screen.getByLabelText("Enable guest access")).toBeChecked();
|
|
});
|
|
});
|
|
|
|
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", async () => {
|
|
const room = new Room(roomId, client, userId);
|
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
|
setRoomStateEvents(room);
|
|
|
|
getComponent(room);
|
|
|
|
await waitFor(() =>
|
|
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);
|
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(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", async () => {
|
|
const room = new Room(roomId, client, userId);
|
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
|
setRoomStateEvents(room);
|
|
getComponent(room);
|
|
|
|
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
|
|
// can't disable encryption once enabled
|
|
expect(screen.getByLabelText("Encrypted")).toBeDisabled();
|
|
});
|
|
|
|
it("asks users to confirm when setting room to encrypted", async () => {
|
|
const room = new Room(roomId, client, userId);
|
|
setRoomStateEvents(room);
|
|
getComponent(room);
|
|
|
|
await waitFor(() => 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);
|
|
|
|
await waitFor(() => 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);
|
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(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", async () => {
|
|
// rooms that are already encrypted still show encrypted
|
|
const room = new Room(roomId, client, userId);
|
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
|
setRoomStateEvents(room);
|
|
getComponent(room);
|
|
|
|
await waitFor(() => expect(screen.getByLabelText("Encrypted")).toBeChecked());
|
|
expect(screen.getByLabelText("Encrypted")).toBeDisabled();
|
|
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
|
|
});
|
|
|
|
it("displays unencrypted rooms with toggle disabled", async () => {
|
|
const room = new Room(roomId, client, userId);
|
|
setRoomStateEvents(room);
|
|
getComponent(room);
|
|
|
|
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
|
|
expect(screen.getByLabelText("Encrypted")).toBeDisabled();
|
|
expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument();
|
|
expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
});
|