From 39b982a9cee41dc426825c08915f4c3628cd7755 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 18 Jan 2023 03:50:34 +0100 Subject: [PATCH] Fix logout devices on password reset --- src/PasswordReset.ts | 3 +- .../structures/auth/ForgotPassword.tsx | 4 +- .../structures/auth/ForgotPassword-test.tsx | 113 +++++++++++------- 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts index f6661a35f1f4..8b121d2e24b9 100644 --- a/src/PasswordReset.ts +++ b/src/PasswordReset.ts @@ -104,8 +104,9 @@ export default class PasswordReset { ); } - public async setNewPassword(password: string): Promise { + public async setNewPassword(password: string, logoutDevices = false): Promise { this.password = password; + this.logoutDevices = logoutDevices; await this.checkEmailLinkClicked(); } diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index d6e0fea1c17b..bddd1c46c188 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -260,7 +260,7 @@ export default class ForgotPassword extends React.Component { this.phase = Phase.ResettingPassword; try { - await this.reset.setNewPassword(this.state.password); + await this.reset.setNewPassword(this.state.password, this.state.logoutDevices); } catch (err: any) { if (err.httpStatus !== 401) { // 401 = waiting for email verification, else unknown error @@ -303,7 +303,7 @@ export default class ForgotPassword extends React.Component { // Don't retry if the phase changed. For example when going back to email input. while (this.state.phase === Phase.ResettingPassword) { try { - await this.reset.setNewPassword(this.state.password); + await this.reset.setNewPassword(this.state.password, this.state.logoutDevices); this.setState({ phase: Phase.Done }); modal.close(); } catch (e) { diff --git a/test/components/structures/auth/ForgotPassword-test.tsx b/test/components/structures/auth/ForgotPassword-test.tsx index 57eccec0148e..2e1742fceeb5 100644 --- a/test/components/structures/auth/ForgotPassword-test.tsx +++ b/test/components/structures/auth/ForgotPassword-test.tsx @@ -49,12 +49,17 @@ describe("", () => { }); }; - const clickButton = async (label: string): Promise => { + const click = async (element: Element): Promise => { await act(async () => { - await userEvent.click(screen.getByText(label), { delay: null }); + await userEvent.click(element, { delay: null }); }); }; + const waitForDialog = async (): Promise => { + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + }; + const itShouldCloseTheDialogAndShowThePasswordInput = (): void => { it("should close the dialog and show the password input", () => { expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); @@ -121,9 +126,9 @@ describe("", () => { }); }); - describe("when clicking »Sign in instead«", () => { + describe("and clicking »Sign in instead«", () => { beforeEach(async () => { - await clickButton("Sign in instead"); + await click(screen.getByText("Sign in instead")); }); it("should call onLoginClick()", () => { @@ -131,7 +136,7 @@ describe("", () => { }); }); - describe("when entering a non-email value", () => { + describe("and entering a non-email value", () => { beforeEach(async () => { await typeIntoField("Email address", "not en email"); }); @@ -141,13 +146,13 @@ describe("", () => { }); }); - describe("when submitting an unknown email", () => { + describe("and submitting an unknown email", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(client).requestPasswordEmailToken.mockRejectedValue({ errcode: "M_THREEPID_NOT_FOUND", }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should show an email not found message", () => { @@ -155,13 +160,13 @@ describe("", () => { }); }); - describe("when a connection error occurs", () => { + describe("and a connection error occurs", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(client).requestPasswordEmailToken.mockRejectedValue({ name: "ConnectionError", }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should show an info about that", () => { @@ -174,7 +179,7 @@ describe("", () => { }); }); - describe("when the server liveness check fails", () => { + describe("and the server liveness check fails", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockRejectedValue({}); @@ -183,7 +188,7 @@ describe("", () => { serverIsAlive: false, serverDeadError: "server down", }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should show the server error", () => { @@ -191,13 +196,13 @@ describe("", () => { }); }); - describe("when submitting an known email", () => { + describe("and submitting an known email", () => { beforeEach(async () => { await typeIntoField("Email address", testEmail); mocked(client).requestPasswordEmailToken.mockResolvedValue({ sid: testSid, }); - await clickButton("Send email"); + await click(screen.getByText("Send email")); }); it("should send the mail and show the check email view", () => { @@ -210,9 +215,9 @@ describe("", () => { expect(screen.getByText(testEmail)).toBeInTheDocument(); }); - describe("when clicking re-enter email", () => { + describe("and clicking »Re-enter email address«", () => { beforeEach(async () => { - await clickButton("Re-enter email address"); + await click(screen.getByText("Re-enter email address")); }); it("go back to the email input", () => { @@ -220,9 +225,9 @@ describe("", () => { }); }); - describe("when clicking resend email", () => { + describe("and clicking »Resend«", () => { beforeEach(async () => { - await userEvent.click(screen.getByText("Resend"), { delay: null }); + await click(screen.getByText("Resend")); // the message is shown after some time jest.advanceTimersByTime(500); }); @@ -237,16 +242,16 @@ describe("", () => { }); }); - describe("when clicking next", () => { + describe("and clicking »Next«", () => { beforeEach(async () => { - await clickButton("Next"); + await click(screen.getByText("Next")); }); it("should show the password input view", () => { expect(screen.getByText("Reset your password")).toBeInTheDocument(); }); - describe("when entering different passwords", () => { + describe("and entering different passwords", () => { beforeEach(async () => { await typeIntoField("New Password", testPassword); await typeIntoField("Confirm new password", testPassword + "asd"); @@ -257,7 +262,7 @@ describe("", () => { }); }); - describe("when entering a new password", () => { + describe("and entering a new password", () => { beforeEach(async () => { mocked(client.setPassword).mockRejectedValue({ httpStatus: 401 }); await typeIntoField("New Password", testPassword); @@ -273,7 +278,7 @@ describe("", () => { retry_after_ms: (13 * 60 + 37) * 1000, }, }); - await clickButton("Reset password"); + await click(screen.getByText("Reset password")); }); it("should show the rate limit error message", () => { @@ -285,10 +290,8 @@ describe("", () => { describe("and submitting it", () => { beforeEach(async () => { - await clickButton("Reset password"); - // double flush promises for the modal to appear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await click(screen.getByText("Reset password")); + await waitForDialog(); }); it("should send the new password and show the click validation link dialog", () => { @@ -316,9 +319,7 @@ describe("", () => { await act(async () => { await userEvent.click(screen.getByTestId("dialog-background"), { delay: null }); }); - // double flush promises for the modal to disappear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await waitForDialog(); }); itShouldCloseTheDialogAndShowThePasswordInput(); @@ -326,23 +327,17 @@ describe("", () => { describe("and dismissing the dialog", () => { beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByLabelText("Close dialog"), { delay: null }); - }); - // double flush promises for the modal to disappear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await click(screen.getByLabelText("Close dialog")); + await waitForDialog(); }); itShouldCloseTheDialogAndShowThePasswordInput(); }); - describe("when clicking re-enter email", () => { + describe("and clicking »Re-enter email address«", () => { beforeEach(async () => { - await clickButton("Re-enter email address"); - // double flush promises for the modal to disappear - await flushPromisesWithFakeTimers(); - await flushPromisesWithFakeTimers(); + await click(screen.getByText("Re-enter email address")); + await waitForDialog(); }); it("should close the dialog and go back to the email input", () => { @@ -351,7 +346,7 @@ describe("", () => { }); }); - describe("when validating the link from the mail", () => { + describe("and validating the link from the mail", () => { beforeEach(async () => { mocked(client.setPassword).mockResolvedValue({}); // be sure the next set password attempt was sent @@ -369,6 +364,42 @@ describe("", () => { }); }); }); + + describe("and clicking »Sign out of all devices« and »Reset password«", () => { + beforeEach(async () => { + await click(screen.getByText("Sign out of all devices")); + await click(screen.getByText("Reset password")); + await waitForDialog(); + }); + + it("should show the sign out warning dialog", async () => { + expect( + screen.getByText( + "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", + ), + ).toBeInTheDocument(); + + // confirm dialog + await click(screen.getByText("Continue")); + + // expect setPassword with logoutDevices = true + expect(client.setPassword).toHaveBeenCalledWith( + { + type: "m.login.email.identity", + threepid_creds: { + client_secret: expect.any(String), + sid: testSid, + }, + threepidCreds: { + client_secret: expect.any(String), + sid: testSid, + }, + }, + testPassword, + true, + ); + }); + }); }); }); });