Use semantic headings in user settings Security (#10774)

* split SettingsSection out of SettingsTab, replace usage

* correct copyright

* use semantic headings in GeneralRoomSettingsTab

* use SettingsTab and SettingsSubsection in room settings

* fix VoipRoomSettingsTab

* use SettingsSection components in space settings

* settingssubsection text component

* use semantic headings in HelpUserSetttings tab

* use ExternalLink components for external links

* test

* strict

* lint

* semantic heading in labs settings

* semantic headings in keyboard settings tab

* semantic heading in preferencesusersettingstab

* tidying

* use new settings components in eventindexpanel

* findByTestId

* prettier

* semantic headings and style refresh for crypto settings

* e2e panel

* test cross signing panel

* strict

* more strict

* tweak

* test eventindexpanel

* strict fixes
This commit is contained in:
Kerry 2023-05-19 10:32:10 +12:00 committed by GitHub
parent 6c262fff6b
commit d9a61c093c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 721 additions and 303 deletions

View file

@ -0,0 +1,113 @@
/*
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 { render, screen } from "@testing-library/react";
import { mocked } from "jest-mock";
import CrossSigningPanel from "../../../../src/components/views/settings/CrossSigningPanel";
import {
flushPromises,
getMockClientWithEventEmitter,
mockClientMethodsCrypto,
mockClientMethodsUser,
} from "../../../test-utils";
describe("<CrossSigningPanel />", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsCrypto(),
doesServerSupportUnstableFeature: jest.fn(),
});
const getComponent = () => render(<CrossSigningPanel />);
beforeEach(() => {
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
mockClient.isCrossSigningReady.mockResolvedValue(false);
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockClear().mockResolvedValue(null);
});
it("should render a spinner while loading", () => {
getComponent();
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
it("should render when homeserver does not support cross-signing", async () => {
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
getComponent();
await flushPromises();
expect(screen.getByText("Your homeserver does not support cross-signing.")).toBeInTheDocument();
});
describe("when cross signing is ready", () => {
beforeEach(() => {
mockClient.isCrossSigningReady.mockResolvedValue(true);
});
it("should render when keys are not backed up", async () => {
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
"⚠️ Cross-signing is ready but keys are not backed up.",
);
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
});
it("should render when keys are backed up", async () => {
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockResolvedValue({ test: {} });
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use.");
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
expect(mockClient.crypto!.crossSigningInfo.isStoredInSecretStorage).toHaveBeenCalledWith(
mockClient.crypto!.secretStorage,
);
});
});
describe("when cross signing is not ready", () => {
beforeEach(() => {
mockClient.isCrossSigningReady.mockResolvedValue(false);
});
it("should render when keys are not backed up", async () => {
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("Cross-signing is not set up.");
});
it("should render when keys are backed up", async () => {
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockResolvedValue({ test: {} });
getComponent();
await flushPromises();
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
);
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
expect(mockClient.crypto!.crossSigningInfo.isStoredInSecretStorage).toHaveBeenCalledWith(
mockClient.crypto!.secretStorage,
);
});
});
});

View file

@ -0,0 +1,201 @@
/*
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, within } from "@testing-library/react";
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
import EventIndexPanel from "../../../../src/components/views/settings/EventIndexPanel";
import EventIndexPeg from "../../../../src/indexing/EventIndexPeg";
import EventIndex from "../../../../src/indexing/EventIndex";
import { clearAllModals, flushPromises, getMockClientWithEventEmitter } from "../../../test-utils";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
describe("<EventIndexPanel />", () => {
getMockClientWithEventEmitter({
getRooms: jest.fn().mockReturnValue([]),
});
const getComponent = () => render(<EventIndexPanel />);
beforeEach(() => {
jest.spyOn(EventIndexPeg, "get").mockRestore();
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
jest.spyOn(EventIndexPeg, "initEventIndex").mockClear().mockResolvedValue(true);
jest.spyOn(EventIndexPeg, "deleteEventIndex").mockClear();
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
jest.spyOn(SettingsStore, "setValue").mockClear();
// @ts-ignore private property
EventIndexPeg.error = null;
});
afterEach(async () => {
await clearAllModals();
});
describe("when event index is initialised", () => {
it("renders event index information", () => {
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it("opens event index management dialog", async () => {
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
getComponent();
fireEvent.click(screen.getByText("Manage"));
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText("Message search")).toBeInTheDocument();
// close the modal
fireEvent.click(within(dialog).getByText("Done"));
});
});
describe("when event indexing is fully supported and enabled but not initialised", () => {
beforeEach(() => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
// @ts-ignore private property
EventIndexPeg.error = { message: "Test error message" };
});
it("displays an error when no event index is found and enabling not in progress", () => {
getComponent();
expect(screen.getByText("Message search initialisation failed")).toBeInTheDocument();
});
it("displays an error from the event index", () => {
getComponent();
expect(screen.getByText("Test error message")).toBeInTheDocument();
});
it("asks for confirmation when resetting seshat", async () => {
getComponent();
fireEvent.click(screen.getByText("Reset"));
// wait for reset modal to open
await screen.findByText("Reset event store?");
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText("Reset event store?")).toBeInTheDocument();
fireEvent.click(within(dialog).getByText("Cancel"));
// didn't reset
expect(SettingsStore.setValue).not.toHaveBeenCalled();
expect(EventIndexPeg.deleteEventIndex).not.toHaveBeenCalled();
});
it("resets seshat", async () => {
getComponent();
fireEvent.click(screen.getByText("Reset"));
// wait for reset modal to open
await screen.findByText("Reset event store?");
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByText("Reset event store"));
await flushPromises();
expect(SettingsStore.setValue).toHaveBeenCalledWith(
"enableEventIndexing",
null,
SettingLevel.DEVICE,
false,
);
expect(EventIndexPeg.deleteEventIndex).toHaveBeenCalled();
await clearAllModals();
});
});
describe("when event indexing is supported but not enabled", () => {
it("renders enable text", () => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
getComponent();
expect(
screen.getByText("Securely cache encrypted messages locally for them to appear in search results."),
).toBeInTheDocument();
});
it("enables event indexing on enable button click", async () => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
let deferredInitEventIndex: IDeferred<boolean> | undefined;
jest.spyOn(EventIndexPeg, "initEventIndex").mockImplementation(() => {
deferredInitEventIndex = defer<boolean>();
return deferredInitEventIndex.promise;
});
getComponent();
fireEvent.click(screen.getByText("Enable"));
await flushPromises();
// spinner shown while enabling
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
// add an event indx to the peg and resolve the init promise
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
expect(EventIndexPeg.initEventIndex).toHaveBeenCalled();
deferredInitEventIndex!.resolve(true);
await flushPromises();
expect(SettingsStore.setValue).toHaveBeenCalledWith("enableEventIndexing", null, SettingLevel.DEVICE, true);
// message for enabled event index
expect(
screen.getByText(
"Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.",
),
).toBeInTheDocument();
});
});
describe("when event indexing is supported but not installed", () => {
it("renders link to install seshat", () => {
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
});
describe("when event indexing is not supported", () => {
it("renders link to download a desktop client", () => {
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CrossSigningPanel /> when cross signing is not ready should render when keys are backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
in secret storage
</td>
</tr>
`;
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
in secret storage
</td>
</tr>
`;
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are not backed up 1`] = `
<tr>
<th
scope="row"
>
Cross-signing private keys:
</th>
<td>
not found in storage
</td>
</tr>
`;

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<EventIndexPanel /> when event index is initialised renders event index information 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Manage
</div>
</div>
`;
exports[`<EventIndexPanel /> when event indexing is not supported renders link to download a desktop client 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
<span>
Element can't securely cache encrypted messages locally while running in a web browser. Use
<a
class="mx_ExternalLink"
href="https://element.io/get-started"
rel="noreferrer noopener"
target="_blank"
>
Element Desktop
<i
class="mx_ExternalLink_icon"
/>
</a>
for encrypted messages to appear in search results.
</span>
</div>
</div>
`;
exports[`<EventIndexPanel /> when event indexing is supported but not installed renders link to install seshat 1`] = `
<div>
<div
class="mx_SettingsSubsection_text"
>
<span>
Element is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Element Desktop with
<a
class="mx_ExternalLink"
href="https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md#adding-seshat-for-search-in-e2e-encrypted-rooms"
rel="noreferrer noopener"
target="_blank"
>
search components added
<i
class="mx_ExternalLink_icon"
/>
</a>
.
</span>
</div>
</div>
`;

View file

@ -2,114 +2,118 @@
exports[`<SecureBackupPanel /> suggests connecting session to key backup when backup exists 1`] = `
<div>
<div>
<p>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
</p>
<p>
<span>
This session is
<b>
not backing up your keys
</b>
, but you do have an existing backup you can restore from and add to going forward.
</span>
</p>
<p>
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
</p>
<details>
<summary>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
<tr>
<th
scope="row"
>
Backup version:
</th>
<td>
1
</td>
</tr>
<tr>
<th
scope="row"
>
Algorithm:
</th>
<td>
test
</td>
</tr>
</table>
<div>
Backup is not signed by any of your sessions
</div>
<div />
</details>
<div
class="mx_SecureBackupPanel_buttonRow"
<div
class="mx_SettingsSubsection_text"
>
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
</div>
<div
class="mx_SettingsSubsection_text"
>
<span>
This session is
<b>
not backing up your keys
</b>
, but you do have an existing backup you can restore from and add to going forward.
</span>
</div>
<div
class="mx_SettingsSubsection_text"
>
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
</div>
<details>
<summary>
Advanced
</summary>
<table
class="mx_SecureBackupPanel_statusList"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Connect this session to Key Backup
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Delete Backup
</div>
<tr>
<th
scope="row"
>
Backup key stored:
</th>
<td>
not stored
</td>
</tr>
<tr>
<th
scope="row"
>
Backup key cached:
</th>
<td>
not found locally
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage public key:
</th>
<td>
not found
</td>
</tr>
<tr>
<th
scope="row"
>
Secret storage:
</th>
<td>
not ready
</td>
</tr>
<tr>
<th
scope="row"
>
Backup version:
</th>
<td>
1
</td>
</tr>
<tr>
<th
scope="row"
>
Algorithm:
</th>
<td>
test
</td>
</tr>
</table>
<div>
Backup is not signed by any of your sessions
</div>
<div />
</details>
<div
class="mx_SecureBackupPanel_buttonRow"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Connect this session to Key Backup
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Delete Backup
</div>
</div>
</div>