From a87362a0487444fb88491db288bd17e2263cbb3f Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 28 Jun 2023 11:45:11 +1200 Subject: [PATCH] Unit test token login flow in `MatrixChat` (#11143) * test tokenlogin * whitespace * tidy * strict --- src/Lifecycle.ts | 2 +- .../components/structures/MatrixChat-test.tsx | 183 +++++++++++++++++- 2 files changed, 178 insertions(+), 7 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index a1d00d86ad..9a2f680950 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -664,7 +664,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise", () => { }; const getComponent = (props: Partial> = {}) => render(); - const localStorageSpy = jest.spyOn(localStorage.__proto__, "getItem").mockReturnValue(undefined); + const localStorageSetSpy = jest.spyOn(localStorage.__proto__, "setItem"); + const localStorageGetSpy = jest.spyOn(localStorage.__proto__, "getItem").mockReturnValue(undefined); + const localStorageClearSpy = jest.spyOn(localStorage.__proto__, "clear"); + const sessionStorageSetSpy = jest.spyOn(sessionStorage.__proto__, "setItem"); + + // make test results readable + filterConsole("Failed to parse localStorage object"); beforeEach(async () => { mockClient = getMockClientWithEventEmitter(getMockClientMethods()); @@ -124,10 +130,14 @@ describe("", () => { unstable_features: {}, versions: [], }); - localStorageSpy.mockReset(); + localStorageGetSpy.mockReset(); + localStorageSetSpy.mockReset(); + sessionStorageSetSpy.mockReset(); jest.spyOn(StorageManager, "idbLoad").mockRestore(); jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined); jest.spyOn(defaultDispatcher, "dispatch").mockClear(); + + await clearAllModals(); }); it("should render spinner while app is loading", () => { @@ -151,7 +161,7 @@ describe("", () => { }; beforeEach(() => { - localStorageSpy.mockImplementation((key: unknown) => mockLocalStorage[key as string] || ""); + localStorageGetSpy.mockImplementation((key: unknown) => mockLocalStorage[key as string] || ""); jest.spyOn(StorageManager, "idbLoad").mockImplementation(async (table, key) => { const safeKey = Array.isArray(key) ? key[0] : key; @@ -350,9 +360,6 @@ describe("", () => { 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 @@ -535,4 +542,168 @@ describe("", () => { }); }); }); + + describe("when query params have a loginToken", () => { + const loginToken = "test-login-token"; + const realQueryParams = { + loginToken, + }; + + const mockLocalStorage: Record = { + mx_sso_hs_url: serverConfig.hsUrl, + mx_sso_is_url: serverConfig.isUrl, + // these are only going to be set during login + mx_hs_url: serverConfig.hsUrl, + mx_is_url: serverConfig.isUrl, + }; + + let loginClient!: ReturnType; + const userId = "@alice:server.org"; + const deviceId = "test-device-id"; + const accessToken = "test-access-token"; + const clientLoginResponse = { + user_id: userId, + device_id: deviceId, + access_token: accessToken, + }; + + beforeEach(() => { + loginClient = getMockClientWithEventEmitter(getMockClientMethods()); + // this is used to create a temporary client during login + jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); + + loginClient.login.mockClear().mockResolvedValue(clientLoginResponse); + + localStorageGetSpy.mockImplementation((key: unknown) => mockLocalStorage[key as string] || ""); + }); + + it("should show an error dialog when no homeserver is found in local storage", async () => { + localStorageGetSpy.mockReturnValue(undefined); + getComponent({ realQueryParams }); + + expect(localStorageGetSpy).toHaveBeenCalledWith("mx_sso_hs_url"); + expect(localStorageGetSpy).toHaveBeenCalledWith("mx_sso_is_url"); + + const dialog = await screen.findByRole("dialog"); + + // warning dialog + expect( + within(dialog).getByText( + "We asked the browser to remember which homeserver you use to let you sign in, " + + "but unfortunately your browser has forgotten it. Go to the sign in page and try again.", + ), + ).toBeInTheDocument(); + }); + + it("should attempt token login", async () => { + getComponent({ realQueryParams }); + + expect(loginClient.login).toHaveBeenCalledWith("m.login.token", { + initial_device_display_name: undefined, + token: loginToken, + }); + }); + + it("should call onTokenLoginCompleted", async () => { + const onTokenLoginCompleted = jest.fn(); + getComponent({ realQueryParams, onTokenLoginCompleted }); + + await flushPromises(); + + expect(onTokenLoginCompleted).toHaveBeenCalled(); + }); + + describe("when login fails", () => { + beforeEach(() => { + loginClient.login.mockRejectedValue(new Error("oups")); + }); + it("should show a dialog", async () => { + getComponent({ realQueryParams }); + + await flushPromises(); + + const dialog = await screen.findByRole("dialog"); + + // warning dialog + expect( + within(dialog).getByText( + "There was a problem communicating with the homeserver, please try again later.", + ), + ).toBeInTheDocument(); + }); + + it("should not clear storage", async () => { + getComponent({ realQueryParams }); + + await flushPromises(); + + expect(loginClient.clearStores).not.toHaveBeenCalled(); + }); + }); + + describe("when login succeeds", () => { + beforeEach(() => { + jest.spyOn(StorageManager, "idbLoad").mockImplementation( + async (_table: string, key: string | string[]) => { + if (key === "mx_access_token") { + return accessToken as any; + } + }, + ); + }); + it("should clear storage", async () => { + getComponent({ realQueryParams }); + + await flushPromises(); + + // just check we called the clearStorage function + expect(loginClient.clearStores).toHaveBeenCalled(); + expect(localStorageClearSpy).toHaveBeenCalled(); + }); + + it("should persist login credentials", async () => { + getComponent({ realQueryParams }); + + await flushPromises(); + + expect(localStorageSetSpy).toHaveBeenCalledWith("mx_hs_url", serverConfig.hsUrl); + expect(localStorageSetSpy).toHaveBeenCalledWith("mx_user_id", userId); + expect(localStorageSetSpy).toHaveBeenCalledWith("mx_has_access_token", "true"); + expect(localStorageSetSpy).toHaveBeenCalledWith("mx_device_id", deviceId); + }); + + it("should set fresh login flag in session storage", async () => { + getComponent({ realQueryParams }); + + await flushPromises(); + + expect(sessionStorageSetSpy).toHaveBeenCalledWith("mx_fresh_login", "true"); + }); + + it("should override hsUrl in creds when login response wellKnown differs from config", async () => { + const hsUrlFromWk = "https://hsfromwk.org"; + const loginResponseWithWellKnown = { + ...clientLoginResponse, + well_known: { + "m.homeserver": { + base_url: hsUrlFromWk, + }, + }, + }; + loginClient.login.mockResolvedValue(loginResponseWithWellKnown); + getComponent({ realQueryParams }); + + await flushPromises(); + + expect(localStorageSetSpy).toHaveBeenCalledWith("mx_hs_url", hsUrlFromWk); + }); + + it("should continue to post login setup when no session is found in local storage", async () => { + getComponent({ realQueryParams }); + + // logged in but waiting for sync screen + await screen.findByText("Logout"); + }); + }); + }); });