2023-01-27 14:06:10 +03:00
|
|
|
/*
|
2024-09-09 16:57:16 +03:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2023-01-27 14:06:10 +03:00
|
|
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
2024-09-09 16:57:16 +03:00
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2023-01-27 14:06:10 +03:00
|
|
|
*/
|
2023-05-17 10:52:44 +03:00
|
|
|
|
2024-10-14 19:11:58 +03:00
|
|
|
import { fireEvent, render, screen, within } from "jest-matrix-react";
|
2023-01-27 14:06:10 +03:00
|
|
|
import React from "react";
|
2024-08-06 20:05:08 +03:00
|
|
|
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
2023-05-17 10:52:44 +03:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2024-08-06 20:05:08 +03:00
|
|
|
import userEvent from "@testing-library/user-event";
|
|
|
|
import { MockedObject } from "jest-mock";
|
2023-01-27 14:06:10 +03:00
|
|
|
|
2024-10-15 16:57:26 +03:00
|
|
|
import AccountUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/AccountUserSettingsTab";
|
|
|
|
import { SdkContextClass, SDKContext } from "../../../../../../../src/contexts/SDKContext";
|
|
|
|
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
2023-01-27 14:06:10 +03:00
|
|
|
import {
|
|
|
|
getMockClientWithEventEmitter,
|
|
|
|
mockClientMethodsServer,
|
|
|
|
mockClientMethodsUser,
|
|
|
|
mockPlatformPeg,
|
|
|
|
flushPromises,
|
2024-10-15 16:57:26 +03:00
|
|
|
} from "../../../../../../test-utils";
|
|
|
|
import { UIFeature } from "../../../../../../../src/settings/UIFeature";
|
|
|
|
import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
|
|
|
|
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
|
|
|
|
import Modal from "../../../../../../../src/Modal";
|
2024-08-06 20:05:08 +03:00
|
|
|
|
|
|
|
let changePasswordOnError: (e: Error) => void;
|
|
|
|
let changePasswordOnFinished: () => void;
|
|
|
|
|
|
|
|
jest.mock(
|
2024-10-15 16:57:26 +03:00
|
|
|
"../../../../../../../src/components/views/settings/ChangePassword",
|
2024-08-06 20:05:08 +03:00
|
|
|
() =>
|
|
|
|
({ onError, onFinished }: { onError: (e: Error) => void; onFinished: () => void }) => {
|
|
|
|
changePasswordOnError = onError;
|
|
|
|
changePasswordOnFinished = onFinished;
|
|
|
|
return <button>Mock change password</button>;
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
describe("<AccountUserSettingsTab />", () => {
|
2023-01-27 14:06:10 +03:00
|
|
|
const defaultProps = {
|
|
|
|
closeSettingsFn: jest.fn(),
|
|
|
|
};
|
|
|
|
|
|
|
|
const userId = "@alice:server.org";
|
2024-08-06 20:05:08 +03:00
|
|
|
let mockClient: MockedObject<MatrixClient>;
|
2023-01-27 14:06:10 +03:00
|
|
|
|
2023-10-16 02:03:25 +03:00
|
|
|
let stores: SdkContextClass;
|
|
|
|
|
2023-01-27 14:06:10 +03:00
|
|
|
const getComponent = () => (
|
2024-06-06 20:35:44 +03:00
|
|
|
<MatrixClientContext.Provider value={mockClient}>
|
|
|
|
<SDKContext.Provider value={stores}>
|
2024-08-06 20:05:08 +03:00
|
|
|
<AccountUserSettingsTab {...defaultProps} />
|
2024-06-06 20:35:44 +03:00
|
|
|
</SDKContext.Provider>
|
|
|
|
</MatrixClientContext.Provider>
|
2023-01-27 14:06:10 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2023-08-22 09:07:10 +03:00
|
|
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
2023-01-27 14:06:10 +03:00
|
|
|
mockPlatformPeg();
|
|
|
|
jest.clearAllMocks();
|
2023-05-17 10:52:44 +03:00
|
|
|
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
|
|
|
jest.spyOn(logger, "error").mockRestore();
|
2023-08-22 09:07:10 +03:00
|
|
|
|
2024-08-06 20:05:08 +03:00
|
|
|
mockClient = getMockClientWithEventEmitter({
|
|
|
|
...mockClientMethodsUser(userId),
|
|
|
|
...mockClientMethodsServer(),
|
|
|
|
getCapabilities: jest.fn(),
|
|
|
|
getThreePids: jest.fn(),
|
|
|
|
getIdentityServerUrl: jest.fn(),
|
|
|
|
deleteThreePid: jest.fn(),
|
|
|
|
});
|
|
|
|
|
2023-08-22 09:07:10 +03:00
|
|
|
mockClient.getCapabilities.mockResolvedValue({});
|
|
|
|
mockClient.getThreePids.mockResolvedValue({
|
|
|
|
threepids: [],
|
|
|
|
});
|
|
|
|
mockClient.deleteThreePid.mockResolvedValue({
|
|
|
|
id_server_unbind_result: "success",
|
|
|
|
});
|
2023-10-16 02:03:25 +03:00
|
|
|
|
|
|
|
stores = new SdkContextClass();
|
|
|
|
stores.client = mockClient;
|
|
|
|
// stub out this store completely to avoid mocking initialisation
|
|
|
|
const mockOidcClientStore = {} as unknown as OidcClientStore;
|
|
|
|
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
2023-01-27 14:06:10 +03:00
|
|
|
});
|
|
|
|
|
2024-08-06 20:05:08 +03:00
|
|
|
afterEach(() => {
|
|
|
|
jest.restoreAllMocks();
|
|
|
|
});
|
|
|
|
|
2023-01-27 14:06:10 +03:00
|
|
|
it("does not show account management link when not available", () => {
|
|
|
|
const { queryByTestId } = render(getComponent());
|
|
|
|
|
|
|
|
expect(queryByTestId("external-account-management-outer")).toBeFalsy();
|
|
|
|
expect(queryByTestId("external-account-management-link")).toBeFalsy();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("show account management link in expected format", async () => {
|
|
|
|
const accountManagementLink = "https://id.server.org/my-account";
|
2023-10-16 02:03:25 +03:00
|
|
|
const mockOidcClientStore = {
|
|
|
|
accountManagementEndpoint: accountManagementLink,
|
|
|
|
} as unknown as OidcClientStore;
|
|
|
|
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
|
|
|
|
2024-07-04 12:46:26 +03:00
|
|
|
render(getComponent());
|
2023-01-27 14:06:10 +03:00
|
|
|
|
2024-07-04 12:46:26 +03:00
|
|
|
const manageAccountLink = await screen.findByRole("button", { name: "Manage account" });
|
|
|
|
expect(manageAccountLink.getAttribute("href")).toMatch(accountManagementLink);
|
2023-01-27 14:06:10 +03:00
|
|
|
});
|
2023-05-17 10:52:44 +03:00
|
|
|
|
|
|
|
describe("deactive account", () => {
|
|
|
|
it("should not render section when account deactivation feature is disabled", () => {
|
|
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
|
|
|
(settingName) => settingName !== UIFeature.Deactivate,
|
|
|
|
);
|
|
|
|
render(getComponent());
|
|
|
|
|
2023-09-26 20:35:55 +03:00
|
|
|
expect(screen.queryByText("Deactivate Account")).not.toBeInTheDocument();
|
2023-05-17 10:52:44 +03:00
|
|
|
expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.Deactivate);
|
|
|
|
});
|
2023-08-22 14:51:16 +03:00
|
|
|
it("should not render section when account is managed externally", async () => {
|
|
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
|
|
|
(settingName) => settingName === UIFeature.Deactivate,
|
|
|
|
);
|
|
|
|
// account is managed externally when we have delegated auth configured
|
2023-10-16 02:03:25 +03:00
|
|
|
const accountManagementLink = "https://id.server.org/my-account";
|
|
|
|
const mockOidcClientStore = {
|
|
|
|
accountManagementEndpoint: accountManagementLink,
|
|
|
|
} as unknown as OidcClientStore;
|
|
|
|
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
2023-08-22 14:51:16 +03:00
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
2023-09-26 20:35:55 +03:00
|
|
|
expect(screen.queryByText("Deactivate Account")).not.toBeInTheDocument();
|
2023-08-22 14:51:16 +03:00
|
|
|
});
|
2023-05-17 10:52:44 +03:00
|
|
|
it("should render section when account deactivation feature is enabled", () => {
|
|
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
|
|
|
(settingName) => settingName === UIFeature.Deactivate,
|
|
|
|
);
|
|
|
|
render(getComponent());
|
|
|
|
|
2023-09-26 20:35:55 +03:00
|
|
|
expect(screen.getByText("Deactivate Account", { selector: "h2" }).parentElement!).toMatchSnapshot();
|
2023-05-17 10:52:44 +03:00
|
|
|
});
|
2024-08-06 20:05:08 +03:00
|
|
|
it("should display the deactivate account dialog when clicked", async () => {
|
|
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
|
|
|
(settingName) => settingName === UIFeature.Deactivate,
|
|
|
|
);
|
|
|
|
|
|
|
|
const createDialogFn = jest.fn();
|
|
|
|
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
|
|
|
|
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
|
|
|
|
|
|
|
|
expect(createDialogFn).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
it("should close settings if account deactivated", async () => {
|
|
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
|
|
|
(settingName) => settingName === UIFeature.Deactivate,
|
|
|
|
);
|
|
|
|
|
|
|
|
const createDialogFn = jest.fn();
|
|
|
|
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
|
|
|
|
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
|
|
|
|
|
|
|
|
createDialogFn.mock.calls[0][1].onFinished(true);
|
|
|
|
|
|
|
|
expect(defaultProps.closeSettingsFn).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
it("should not close settings if account not deactivated", async () => {
|
|
|
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
|
|
|
(settingName) => settingName === UIFeature.Deactivate,
|
|
|
|
);
|
|
|
|
|
|
|
|
const createDialogFn = jest.fn();
|
|
|
|
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
|
|
|
|
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" }));
|
|
|
|
|
|
|
|
createDialogFn.mock.calls[0][1].onFinished(false);
|
|
|
|
|
|
|
|
expect(defaultProps.closeSettingsFn).not.toHaveBeenCalled();
|
|
|
|
});
|
2023-05-17 10:52:44 +03:00
|
|
|
});
|
2023-08-22 09:07:10 +03:00
|
|
|
|
|
|
|
describe("3pids", () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
mockClient.getCapabilities.mockResolvedValue({
|
|
|
|
"m.3pid_changes": {
|
|
|
|
enabled: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
mockClient.getThreePids.mockResolvedValue({
|
|
|
|
threepids: [
|
|
|
|
{
|
|
|
|
medium: ThreepidMedium.Email,
|
|
|
|
address: "test@test.io",
|
|
|
|
validated_at: 1685067124552,
|
|
|
|
added_at: 1685067124552,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
medium: ThreepidMedium.Phone,
|
|
|
|
address: "123456789",
|
|
|
|
validated_at: 1685067124552,
|
|
|
|
added_at: 1685067124552,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
mockClient.getIdentityServerUrl.mockReturnValue(undefined);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should show loaders while 3pids load", () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
expect(
|
|
|
|
within(screen.getByTestId("mx_AccountEmailAddresses")).getByLabelText("Loading…"),
|
|
|
|
).toBeInTheDocument();
|
|
|
|
expect(within(screen.getByTestId("mx_AccountPhoneNumbers")).getByLabelText("Loading…")).toBeInTheDocument();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should display 3pid email addresses and phone numbers", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
expect(screen.getByTestId("mx_AccountEmailAddresses")).toMatchSnapshot();
|
|
|
|
expect(screen.getByTestId("mx_AccountPhoneNumbers")).toMatchSnapshot();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should allow removing an existing email addresses", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
|
|
|
|
|
|
|
fireEvent.click(within(section).getByText("Remove"));
|
|
|
|
|
|
|
|
// confirm removal
|
|
|
|
expect(screen.getByText("Remove test@test.io?")).toBeInTheDocument();
|
|
|
|
fireEvent.click(within(section).getByText("Remove"));
|
|
|
|
|
|
|
|
expect(mockClient.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Email, "test@test.io");
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should allow adding a new email address", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
|
|
|
|
|
|
|
// just check the fields are enabled
|
|
|
|
expect(within(section).getByLabelText("Email Address")).not.toBeDisabled();
|
|
|
|
expect(within(section).getByText("Add")).not.toHaveAttribute("aria-disabled");
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should allow removing an existing phone number", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
|
|
|
|
|
|
|
fireEvent.click(within(section).getByText("Remove"));
|
|
|
|
|
|
|
|
// confirm removal
|
|
|
|
expect(screen.getByText("Remove 123456789?")).toBeInTheDocument();
|
|
|
|
fireEvent.click(within(section).getByText("Remove"));
|
|
|
|
|
|
|
|
expect(mockClient.deleteThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, "123456789");
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should allow adding a new phone number", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
|
|
|
|
|
|
|
// just check the fields are enabled
|
|
|
|
expect(within(section).getByLabelText("Phone Number")).not.toBeDisabled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should allow 3pid changes when capabilities does not have 3pid_changes", async () => {
|
|
|
|
// We support as far back as v1.1 which doesn't have m.3pid_changes
|
|
|
|
// so the behaviour for when it is missing has to be assume true
|
|
|
|
mockClient.getCapabilities.mockResolvedValue({});
|
|
|
|
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
|
|
|
|
|
|
|
// just check the fields are enabled
|
|
|
|
expect(within(section).getByLabelText("Email Address")).not.toBeDisabled();
|
|
|
|
expect(within(section).getByText("Add")).not.toHaveAttribute("aria-disabled");
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("when 3pid changes capability is disabled", () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
mockClient.getCapabilities.mockResolvedValue({
|
|
|
|
"m.3pid_changes": {
|
|
|
|
enabled: false,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should not allow removing email addresses", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
|
|
|
|
|
|
|
expect(within(section).getByText("Remove")).toHaveAttribute("aria-disabled");
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should not allow adding a new email addresses", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountEmailAddresses");
|
|
|
|
|
|
|
|
// fields are not enabled
|
|
|
|
expect(within(section).getByLabelText("Email Address")).toBeDisabled();
|
|
|
|
expect(within(section).getByText("Add")).toHaveAttribute("aria-disabled");
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should not allow removing phone numbers", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
|
|
|
|
|
|
|
expect(within(section).getByText("Remove")).toHaveAttribute("aria-disabled");
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should not allow adding a new phone number", async () => {
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
const section = screen.getByTestId("mx_AccountPhoneNumbers");
|
|
|
|
|
|
|
|
expect(within(section).getByLabelText("Phone Number")).toBeDisabled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2024-08-06 20:05:08 +03:00
|
|
|
|
|
|
|
describe("Password change", () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
mockClient.getCapabilities.mockResolvedValue({
|
|
|
|
"m.change_password": {
|
|
|
|
enabled: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should display a dialog if password change succeeded", async () => {
|
|
|
|
const createDialogFn = jest.fn();
|
|
|
|
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
|
|
|
|
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
const changeButton = await screen.findByRole("button", { name: "Mock change password" });
|
|
|
|
userEvent.click(changeButton);
|
|
|
|
|
|
|
|
expect(changePasswordOnFinished).toBeDefined();
|
|
|
|
changePasswordOnFinished();
|
|
|
|
|
|
|
|
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
|
|
|
|
title: "Success",
|
|
|
|
description: "Your password was successfully changed.",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should display an error if password change failed", async () => {
|
|
|
|
const ERROR_STRING =
|
|
|
|
"Your password must contain exactly 5 lowercase letters, a box drawing character and the badger emoji.";
|
|
|
|
|
|
|
|
const createDialogFn = jest.fn();
|
|
|
|
jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn);
|
|
|
|
|
|
|
|
render(getComponent());
|
|
|
|
|
|
|
|
const changeButton = await screen.findByRole("button", { name: "Mock change password" });
|
|
|
|
userEvent.click(changeButton);
|
|
|
|
|
|
|
|
expect(changePasswordOnError).toBeDefined();
|
|
|
|
changePasswordOnError(new Error(ERROR_STRING));
|
|
|
|
|
|
|
|
expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), {
|
|
|
|
title: "Error changing password",
|
|
|
|
description: ERROR_STRING,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-01-27 14:06:10 +03:00
|
|
|
});
|