From c95fa19fa1c602196e311022070be5aaaa5acf1b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Sep 2024 15:55:30 +0100 Subject: [PATCH 1/3] Add config option to force verification If this is set, users will not have the option to skip verification on login (they will still be able to reload and continue unverified, currently). Default off. --- src/IConfigOptions.ts | 2 ++ src/components/structures/auth/CompleteSecurity.tsx | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 97346641ec..72bee5d0ab 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -52,6 +52,8 @@ export interface IConfigOptions { auth_footer_links?: { text: string; url: string }[]; }; + force_verification?: boolean; // if true, users must verify new logins + map_style_url?: string; // for location-shared maps embedded_pages?: { diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index eb3ae3f592..568b7bffbd 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -14,6 +14,7 @@ import SetupEncryptionBody from "./SetupEncryptionBody"; import AccessibleButton from "../../views/elements/AccessibleButton"; import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody"; import AuthPage from "../../views/auth/AuthPage"; +import SdkConfig from "../../../SdkConfig"; interface IProps { onFinished: () => void; @@ -82,8 +83,10 @@ export default class CompleteSecurity extends React.Component { throw new Error(`Unknown phase ${phase}`); } + const forceVerification = SdkConfig.get("force_verification") ?? false; + let skipButton; - if (phase === Phase.Intro || phase === Phase.ConfirmReset) { + if (!forceVerification && (phase === Phase.Intro || phase === Phase.ConfirmReset)) { skipButton = ( Date: Wed, 11 Sep 2024 17:29:46 +0100 Subject: [PATCH 2/3] Test for complete security dialog --- .../structures/auth/CompleteSecurity-test.tsx | 77 +++++++++++++++++++ test/test-utils/test-utils.ts | 2 + 2 files changed, 79 insertions(+) create mode 100644 test/components/structures/auth/CompleteSecurity-test.tsx diff --git a/test/components/structures/auth/CompleteSecurity-test.tsx b/test/components/structures/auth/CompleteSecurity-test.tsx new file mode 100644 index 0000000000..d250477f7e --- /dev/null +++ b/test/components/structures/auth/CompleteSecurity-test.tsx @@ -0,0 +1,77 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { mocked } from "jest-mock"; +import EventEmitter from "events"; + +import CompleteSecurity from "../../../../src/components/structures/auth/CompleteSecurity"; +import { stubClient } from "../../../test-utils"; +import { Phase, SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore"; +import SdkConfig from "../../../../src/SdkConfig"; + +class MockSetupEncryptionStore extends EventEmitter { + public phase: Phase = Phase.Intro; + public lostKeys(): boolean { + return false; + } + + public start: () => void = jest.fn(); + public stop: () => void = jest.fn(); +} + +describe("CompleteSecurity", () => { + beforeEach(() => { + const client = stubClient(); + const deviceIdToDevice = new Map(); + deviceIdToDevice.set("DEVICE_ID", { + deviceId: "DEVICE_ID", + userId: "USER_ID", + }); + const userIdToDevices = new Map(); + userIdToDevices.set("USER_ID", deviceIdToDevice); + mocked(client.getCrypto()!.getUserDeviceInfo).mockResolvedValue(userIdToDevices); + + const mockSetupEncryptionStore = new MockSetupEncryptionStore(); + jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(mockSetupEncryptionStore as SetupEncryptionStore); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("Renders with a cancel button by default", () => { + render( {}} />); + + expect(screen.getByRole("button", { name: "Skip verification for now"})).toBeInTheDocument(); + }); + + it("Renders with a cancel button if forceVerification false", () => { + jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => { + if (key === "forceVerification") { + return false; + } + }); + + render( {}} />); + + expect(screen.getByRole("button", { name: "Skip verification for now"})).toBeInTheDocument(); + }); + + it("Renders without a cancel button if forceVerification true", () => { + jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => { + if (key === "force_verification") { + return true; + } + }); + + render( {}} />); + + expect(screen.queryByRole("button", { name: "Skip verification for now"})).not.toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 8e82c298e4..092f30fb52 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -108,6 +108,7 @@ export function createTestClient(): MatrixClient { secretStorage: { get: jest.fn(), + isStored: jest.fn().mockReturnValue(false), }, store: { @@ -128,6 +129,7 @@ export function createTestClient(): MatrixClient { getDeviceVerificationStatus: jest.fn(), resetKeyBackup: jest.fn(), isEncryptionEnabledInRoom: jest.fn(), + getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), }), getPushActionsForEvent: jest.fn(), From 75363c48e54f6aa815e4b44d8a08de17055aee53 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Sep 2024 17:36:20 +0100 Subject: [PATCH 3/3] I hadn't set up prettier --- .../structures/auth/CompleteSecurity-test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/components/structures/auth/CompleteSecurity-test.tsx b/test/components/structures/auth/CompleteSecurity-test.tsx index d250477f7e..295de9eb4f 100644 --- a/test/components/structures/auth/CompleteSecurity-test.tsx +++ b/test/components/structures/auth/CompleteSecurity-test.tsx @@ -38,7 +38,9 @@ describe("CompleteSecurity", () => { mocked(client.getCrypto()!.getUserDeviceInfo).mockResolvedValue(userIdToDevices); const mockSetupEncryptionStore = new MockSetupEncryptionStore(); - jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(mockSetupEncryptionStore as SetupEncryptionStore); + jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue( + mockSetupEncryptionStore as SetupEncryptionStore, + ); }); afterEach(() => { @@ -48,7 +50,7 @@ describe("CompleteSecurity", () => { it("Renders with a cancel button by default", () => { render( {}} />); - expect(screen.getByRole("button", { name: "Skip verification for now"})).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Skip verification for now" })).toBeInTheDocument(); }); it("Renders with a cancel button if forceVerification false", () => { @@ -60,7 +62,7 @@ describe("CompleteSecurity", () => { render( {}} />); - expect(screen.getByRole("button", { name: "Skip verification for now"})).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Skip verification for now" })).toBeInTheDocument(); }); it("Renders without a cancel button if forceVerification true", () => { @@ -72,6 +74,6 @@ describe("CompleteSecurity", () => { render( {}} />); - expect(screen.queryByRole("button", { name: "Skip verification for now"})).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Skip verification for now" })).not.toBeInTheDocument(); }); -}); \ No newline at end of file +});