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
pull/28217/head
Kerry 2023-06-23 08:57:16 +12:00 committed by GitHub
parent d935da2844
commit 0eda8c17d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 180 additions and 5 deletions

View File

@ -17,9 +17,10 @@ limitations under the License.
import React, { ComponentProps } from "react"; import React, { ComponentProps } from "react";
import { fireEvent, render, RenderResult, screen, within } from "@testing-library/react"; import { fireEvent, render, RenderResult, screen, within } from "@testing-library/react";
import fetchMockJest from "fetch-mock-jest"; 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 { SyncState } from "matrix-js-sdk/src/sync";
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler"; 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 { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import MatrixChat from "../../../src/components/structures/MatrixChat"; 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 defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions"; import { Action } from "../../../src/dispatcher/actions";
import { UserTab } from "../../../src/components/views/dialogs/UserTab"; 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"; import * as leaveRoomUtils from "../../../src/utils/leave-behaviour";
describe("<MatrixChat />", () => { describe("<MatrixChat />", () => {
const userId = "@alice:server.org"; const userId = "@alice:server.org";
const deviceId = "qwertyui"; const deviceId = "qwertyui";
const accessToken = "abc123"; const accessToken = "abc123";
const mockClient = getMockClientWithEventEmitter({ // reused in createClient mock below
const getMockClientMethods = () => ({
...mockClientMethodsUser(userId), ...mockClientMethodsUser(userId),
startClient: jest.fn(), startClient: jest.fn(),
stopClient: jest.fn(), stopClient: jest.fn(),
@ -57,7 +65,28 @@ describe("<MatrixChat />", () => {
store: { store: {
destroy: jest.fn(), 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 = { const serverConfig = {
hsUrl: "https://test.com", hsUrl: "https://test.com",
hsName: "Test Server", hsName: "Test Server",
@ -88,12 +117,13 @@ describe("<MatrixChat />", () => {
render(<MatrixChat {...defaultProps} {...props} />); render(<MatrixChat {...defaultProps} {...props} />);
const localStorageSpy = jest.spyOn(localStorage.__proto__, "getItem").mockReturnValue(undefined); const localStorageSpy = jest.spyOn(localStorage.__proto__, "getItem").mockReturnValue(undefined);
beforeEach(() => { beforeEach(async () => {
mockClient = getMockClientWithEventEmitter(getMockClientMethods());
fetchMockJest.get("https://test.com/_matrix/client/versions", { fetchMockJest.get("https://test.com/_matrix/client/versions", {
unstable_features: {}, unstable_features: {},
versions: [], versions: [],
}); });
localStorageSpy.mockClear(); localStorageSpy.mockReset();
jest.spyOn(StorageManager, "idbLoad").mockRestore(); jest.spyOn(StorageManager, "idbLoad").mockRestore();
jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined); jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined);
jest.spyOn(defaultDispatcher, "dispatch").mockClear(); jest.spyOn(defaultDispatcher, "dispatch").mockClear();
@ -130,6 +160,7 @@ describe("<MatrixChat />", () => {
const getComponentAndWaitForReady = async (): Promise<RenderResult> => { const getComponentAndWaitForReady = async (): Promise<RenderResult> => {
const renderResult = getComponent(); const renderResult = getComponent();
// we think we are logged in, but are still waiting for the /sync to complete // we think we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Logout"); await screen.findByText("Logout");
// initial sync // initial sync
@ -306,4 +337,148 @@ describe("<MatrixChat />", () => {
}); });
}); });
}); });
describe("login via key/pass", () => {
let loginClient!: ReturnType<typeof getMockClientWithEventEmitter>;
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<RenderResult> => {
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<void> => {
// 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<void> => {
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();
});
});
});
}); });