From 0eda8c17d570451a79b3c269afb7072be77f5e58 Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 23 Jun 2023 08:57:16 +1200 Subject: [PATCH] Unit test post-login security setup flows in `MatrixChat` (#11126) * shuffle testing functions * test post login security setup flows * remove debug * strict fixes * strict fixes p2 --- .../components/structures/MatrixChat-test.tsx | 185 +++++++++++++++++- 1 file changed, 180 insertions(+), 5 deletions(-) diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index dd81ecf68c..d86d2ba29f 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -17,9 +17,10 @@ limitations under the License. import React, { ComponentProps } from "react"; import { fireEvent, render, RenderResult, screen, within } from "@testing-library/react"; import fetchMockJest from "fetch-mock-jest"; -import { ClientEvent } from "matrix-js-sdk/src/client"; +import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { SyncState } from "matrix-js-sdk/src/sync"; import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler"; +import * as MatrixJs from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import MatrixChat from "../../../src/components/structures/MatrixChat"; @@ -27,14 +28,21 @@ import * as StorageManager from "../../../src/utils/StorageManager"; import defaultDispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; import { UserTab } from "../../../src/components/views/dialogs/UserTab"; -import { clearAllModals, flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils"; +import { + clearAllModals, + filterConsole, + flushPromises, + getMockClientWithEventEmitter, + mockClientMethodsUser, +} from "../../test-utils"; import * as leaveRoomUtils from "../../../src/utils/leave-behaviour"; describe("", () => { const userId = "@alice:server.org"; const deviceId = "qwertyui"; const accessToken = "abc123"; - const mockClient = getMockClientWithEventEmitter({ + // reused in createClient mock below + const getMockClientMethods = () => ({ ...mockClientMethodsUser(userId), startClient: jest.fn(), stopClient: jest.fn(), @@ -57,7 +65,28 @@ describe("", () => { store: { destroy: jest.fn(), }, + login: jest.fn(), + loginFlows: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + clearStores: jest.fn(), + setGuest: jest.fn(), + setNotifTimelineSet: jest.fn(), + getAccountData: jest.fn(), + doesServerSupportUnstableFeature: jest.fn(), + getDevices: jest.fn().mockResolvedValue({ devices: [] }), + getProfileInfo: jest.fn(), + getVisibleRooms: jest.fn().mockReturnValue([]), + getRooms: jest.fn().mockReturnValue([]), + userHasCrossSigningKeys: jest.fn(), + setGlobalBlacklistUnverifiedDevices: jest.fn(), + setGlobalErrorOnUnknownDevices: jest.fn(), + getCrypto: jest.fn(), + secretStorage: { + isStored: jest.fn().mockReturnValue(null), + }, + getDehydratedDevice: jest.fn(), }); + let mockClient = getMockClientWithEventEmitter(getMockClientMethods()); const serverConfig = { hsUrl: "https://test.com", hsName: "Test Server", @@ -88,12 +117,13 @@ describe("", () => { render(); const localStorageSpy = jest.spyOn(localStorage.__proto__, "getItem").mockReturnValue(undefined); - beforeEach(() => { + beforeEach(async () => { + mockClient = getMockClientWithEventEmitter(getMockClientMethods()); fetchMockJest.get("https://test.com/_matrix/client/versions", { unstable_features: {}, versions: [], }); - localStorageSpy.mockClear(); + localStorageSpy.mockReset(); jest.spyOn(StorageManager, "idbLoad").mockRestore(); jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined); jest.spyOn(defaultDispatcher, "dispatch").mockClear(); @@ -130,6 +160,7 @@ describe("", () => { const getComponentAndWaitForReady = async (): Promise => { const renderResult = getComponent(); + // we think we are logged in, but are still waiting for the /sync to complete await screen.findByText("Logout"); // initial sync @@ -306,4 +337,148 @@ describe("", () => { }); }); }); + + describe("login via key/pass", () => { + let loginClient!: ReturnType; + + const mockCrypto = { + getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), + getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()), + }; + + const userName = "ernie"; + const password = "ilovebert"; + + // make test results readable + filterConsole("Failed to parse localStorage object"); + + const getComponentAndWaitForReady = async (): Promise => { + const renderResult = getComponent(); + // wait for welcome page chrome render + await screen.findByText("powered by Matrix"); + + // go to login page + defaultDispatcher.dispatch({ + action: "start_login", + }); + + await flushPromises(); + + return renderResult; + }; + + const waitForSyncAndLoad = async (client: MatrixClient, withoutSecuritySetup?: boolean): Promise => { + // need to wait for different elements depending on which flow + // without security setup we go to a loading page + if (withoutSecuritySetup) { + // we think we are logged in, but are still waiting for the /sync to complete + await screen.findByText("Logout"); + // initial sync + client.emit(ClientEvent.Sync, SyncState.Prepared, null); + // wait for logged in view to load + await screen.findByLabelText("User menu"); + + // otherwise we stay on login and load from there for longer + } else { + // we are logged in, but are still waiting for the /sync to complete + await screen.findByText("Syncing…"); + // initial sync + client.emit(ClientEvent.Sync, SyncState.Prepared, null); + } + + // let things settle + await flushPromises(); + // and some more for good measure + // this proved to be a little flaky + await flushPromises(); + }; + + const getComponentAndLogin = async (withoutSecuritySetup?: boolean): Promise => { + await getComponentAndWaitForReady(); + + fireEvent.change(screen.getByLabelText("Username"), { target: { value: userName } }); + fireEvent.change(screen.getByLabelText("Password"), { target: { value: password } }); + + // sign in button is an input + fireEvent.click(screen.getByDisplayValue("Sign in")); + + await waitForSyncAndLoad(loginClient, withoutSecuritySetup); + }; + + beforeEach(() => { + loginClient = getMockClientWithEventEmitter(getMockClientMethods()); + // this is used to create a temporary client during login + jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); + + loginClient.login.mockClear().mockResolvedValue({}); + loginClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] }); + + loginClient.getProfileInfo.mockResolvedValue({ + displayname: "Ernie", + }); + }); + + it("should render login page", async () => { + await getComponentAndWaitForReady(); + + expect(screen.getAllByText("Sign in")[0]).toBeInTheDocument(); + }); + + describe("post login setup", () => { + beforeEach(() => { + loginClient.isCryptoEnabled.mockReturnValue(true); + loginClient.getCrypto.mockReturnValue(mockCrypto as any); + loginClient.userHasCrossSigningKeys.mockClear().mockResolvedValue(false); + }); + + it("should go straight to logged in view when crypto is not enabled", async () => { + loginClient.isCryptoEnabled.mockReturnValue(false); + + await getComponentAndLogin(true); + + expect(loginClient.userHasCrossSigningKeys).not.toHaveBeenCalled(); + }); + + it("should go straight to logged in view when user does not have cross signing keys and server does not support cross signing", async () => { + loginClient.doesServerSupportUnstableFeature.mockResolvedValue(false); + + await getComponentAndLogin(false); + + expect(loginClient.doesServerSupportUnstableFeature).toHaveBeenCalledWith( + "org.matrix.e2e_cross_signing", + ); + + await flushPromises(); + + // logged in + await screen.findByLabelText("User menu"); + }); + + it("should show complete security screen when user has cross signing setup", async () => { + loginClient.userHasCrossSigningKeys.mockResolvedValue(true); + + await getComponentAndLogin(); + + expect(loginClient.userHasCrossSigningKeys).toHaveBeenCalled(); + + await flushPromises(); + + // Complete security begin screen is rendered + expect(screen.getByText("Unable to verify this device")).toBeInTheDocument(); + }); + + it("should setup e2e when server supports cross signing", async () => { + loginClient.doesServerSupportUnstableFeature.mockResolvedValue(true); + + await getComponentAndLogin(); + + expect(loginClient.userHasCrossSigningKeys).toHaveBeenCalled(); + + await flushPromises(); + + // set up keys screen is rendered + expect(screen.getByText("Setting up keys")).toBeInTheDocument(); + }); + }); + }); });