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 p2pull/28217/head
parent
d935da2844
commit
0eda8c17d5
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue