Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Fix logout devices when resetting the password #9925

Merged
merged 1 commit into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/PasswordReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ export default class PasswordReset {
);
}

public setLogoutDevices(logoutDevices: boolean): void {
this.logoutDevices = logoutDevices;
}

public async setNewPassword(password: string): Promise<void> {
this.password = password;
await this.checkEmailLinkClicked();
Expand Down
1 change: 1 addition & 0 deletions src/components/structures/auth/ForgotPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
}

this.phase = Phase.ResettingPassword;
this.reset.setLogoutDevices(this.state.logoutDevices);

try {
await this.reset.setNewPassword(this.state.password);
Expand Down
113 changes: 72 additions & 41 deletions test/components/structures/auth/ForgotPassword-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,17 @@ describe("<ForgotPassword>", () => {
});
};

const clickButton = async (label: string): Promise<void> => {
const click = async (element: Element): Promise<void> => {
await act(async () => {
await userEvent.click(screen.getByText(label), { delay: null });
await userEvent.click(element, { delay: null });
});
};

const waitForDialog = async (): Promise<void> => {
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();
Expand Down Expand Up @@ -121,17 +126,17 @@ describe("<ForgotPassword>", () => {
});
});

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()", () => {
expect(onLoginClick).toHaveBeenCalled();
});
});

describe("when entering a non-email value", () => {
describe("and entering a non-email value", () => {
beforeEach(async () => {
await typeIntoField("Email address", "not en email");
});
Expand All @@ -141,27 +146,27 @@ describe("<ForgotPassword>", () => {
});
});

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", () => {
expect(screen.getByText("This email address was not found")).toBeInTheDocument();
});
});

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", () => {
Expand All @@ -174,7 +179,7 @@ describe("<ForgotPassword>", () => {
});
});

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({});
Expand All @@ -183,21 +188,21 @@ describe("<ForgotPassword>", () => {
serverIsAlive: false,
serverDeadError: "server down",
});
await clickButton("Send email");
await click(screen.getByText("Send email"));
});

it("should show the server error", () => {
expect(screen.queryByText("server down")).toBeInTheDocument();
});
});

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", () => {
Expand All @@ -210,19 +215,19 @@ describe("<ForgotPassword>", () => {
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", () => {
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
});
});

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);
});
Expand All @@ -237,16 +242,16 @@ describe("<ForgotPassword>", () => {
});
});

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");
Expand All @@ -257,7 +262,7 @@ describe("<ForgotPassword>", () => {
});
});

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);
Expand All @@ -273,7 +278,7 @@ describe("<ForgotPassword>", () => {
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", () => {
Expand All @@ -285,10 +290,8 @@ describe("<ForgotPassword>", () => {

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", () => {
Expand Down Expand Up @@ -316,33 +319,25 @@ describe("<ForgotPassword>", () => {
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();
});

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", () => {
Expand All @@ -351,7 +346,7 @@ describe("<ForgotPassword>", () => {
});
});

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
Expand All @@ -369,6 +364,42 @@ describe("<ForgotPassword>", () => {
});
});
});

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,
);
});
});
});
});
});
Expand Down