riot-web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test...

418 lines
16 KiB
TypeScript
Raw Normal View History

/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { fireEvent, render, screen, within } from "jest-matrix-react";
import React from "react";
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import userEvent from "@testing-library/user-event";
import { MockedObject } from "jest-mock";
import AccountUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/AccountUserSettingsTab";
import { SdkContextClass, SDKContext } from "../../../../../../../src/contexts/SDKContext";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import {
getMockClientWithEventEmitter,
mockClientMethodsServer,
mockClientMethodsUser,
mockPlatformPeg,
flushPromises,
} 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";
let changePasswordOnError: (e: Error) => void;
let changePasswordOnFinished: () => void;
jest.mock(
"../../../../../../../src/components/views/settings/ChangePassword",
() =>
({ onError, onFinished }: { onError: (e: Error) => void; onFinished: () => void }) => {
changePasswordOnError = onError;
changePasswordOnFinished = onFinished;
return <button>Mock change password</button>;
},
);
describe("<AccountUserSettingsTab />", () => {
const defaultProps = {
closeSettingsFn: jest.fn(),
};
const userId = "@alice:server.org";
let mockClient: MockedObject<MatrixClient>;
OIDC: use delegated auth account URL from `OidcClientStore` (#11723) * test persistCredentials without a pickle key * test setLoggedIn with pickle key * lint * type error * extract token persisting code into function, persist refresh token * store has_refresh_token too * pass refreshToken from oidcAuthGrant into credentials * rest restore session with pickle key * retreive stored refresh token and add to credentials * extract token decryption into function * remove TODO * very messy poc * utils to persist clientId and issuer after oidc authentication * add dep oidc-client-ts * persist issuer and clientId after successful oidc auth * add OidcClientStore * comments and tidy * expose getters for stored refresh and access tokens in Lifecycle * revoke tokens with oidc provider * test logout action in MatrixChat * comments * prettier * test OidcClientStore.revokeTokens * put pickle key destruction back * comment pedantry * working refresh without persistence * extract token persistence functions to utils * add sugar * implement TokenRefresher class with persistence * tidying * persist idTokenClaims * persist idTokenClaims * tests * remove unused cde * create token refresher during doSetLoggedIn * tidying * also tidying * OidcClientStore.initClient use stored issuer when client well known unavailable * test Lifecycle.logout * update Lifecycle test replaceUsingCreds calls * fix test * add sdkContext to UserSettingsDialog * use sdkContext and oidcClientStore in session manager * use sdkContext and OidcClientStore in generalusersettingstab * tidy * test tokenrefresher creation in login flow * test token refresher * Update src/utils/oidc/TokenRefresher.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * use literal value for m.authentication Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * improve comments * fix test mock, comment * typo * add sdkContext to SoftLogout, pass oidcClientStore to logout * fullstops * comments * fussy comment formatting --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-16 01:03:25 +02:00
let stores: SdkContextClass;
const getComponent = () => (
Fix display of no avatar in avatar setting controls (#12558) * New user profile UI in User Settings Using new Edit In Place component. * Show avatar upload error * Fix avatar upload error * Wire up errors & feedback for display name setting * Implement avatar upload / remove progress toast * Add 768px breakpoint * Fix display of no avatar in avatar setting controls There was supposed to be a person icon but it was invisible, and also would have been inappropriate for room avatars anyway. This makes it match the designs by being the same as whatever the default avatar is. * Fix room profile display * Update to released compund-web with required components / fixes * Require compound-web 4.4.0 because we do need it * Update snapshots Because of course all the auto-generated IDs of unrelated things have changed. * Fix duplicate import * Fix CSS comment * Update snapshot * Run all the tests so the ids stay the same * Start of a test for ProfileSettings * More tests * Test that a toast appears * Test ToastRack * Update snapshots * Add the usernamee control * Fix playwright tests * New compound version for editinplace fixes * Fix useId to not just generate a constant ID * Use the label in the username component * Fix widths of test boxes * Update screenshots * Put ^ back on compound-web version * Split CSS for room & user profile settings and name the components correspondingly * Fix playwright test * Update room settings screenshot * Use original screenshot instead * Add required props in test * Fix test * Also here * Update screenshots * Remove user icon ...which is unused now, as far as I can see. * Fix styling of unrelated buttons Needed to be added in other places otherwise the specificity changes. Also put the old screenshots back. * Add copyright year * Fix copyright year * Switch to useMatrixClientContext * Fix other test
2024-06-06 19:35:44 +02:00
<MatrixClientContext.Provider value={mockClient}>
<SDKContext.Provider value={stores}>
<AccountUserSettingsTab {...defaultProps} />
Fix display of no avatar in avatar setting controls (#12558) * New user profile UI in User Settings Using new Edit In Place component. * Show avatar upload error * Fix avatar upload error * Wire up errors & feedback for display name setting * Implement avatar upload / remove progress toast * Add 768px breakpoint * Fix display of no avatar in avatar setting controls There was supposed to be a person icon but it was invisible, and also would have been inappropriate for room avatars anyway. This makes it match the designs by being the same as whatever the default avatar is. * Fix room profile display * Update to released compund-web with required components / fixes * Require compound-web 4.4.0 because we do need it * Update snapshots Because of course all the auto-generated IDs of unrelated things have changed. * Fix duplicate import * Fix CSS comment * Update snapshot * Run all the tests so the ids stay the same * Start of a test for ProfileSettings * More tests * Test that a toast appears * Test ToastRack * Update snapshots * Add the usernamee control * Fix playwright tests * New compound version for editinplace fixes * Fix useId to not just generate a constant ID * Use the label in the username component * Fix widths of test boxes * Update screenshots * Put ^ back on compound-web version * Split CSS for room & user profile settings and name the components correspondingly * Fix playwright test * Update room settings screenshot * Use original screenshot instead * Add required props in test * Fix test * Also here * Update screenshots * Remove user icon ...which is unused now, as far as I can see. * Fix styling of unrelated buttons Needed to be added in other places otherwise the specificity changes. Also put the old screenshots back. * Add copyright year * Fix copyright year * Switch to useMatrixClientContext * Fix other test
2024-06-06 19:35:44 +02:00
</SDKContext.Provider>
</MatrixClientContext.Provider>
);
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
mockPlatformPeg();
jest.clearAllMocks();
jest.spyOn(SettingsStore, "getValue").mockRestore();
jest.spyOn(logger, "error").mockRestore();
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
getCapabilities: jest.fn(),
getThreePids: jest.fn(),
getIdentityServerUrl: jest.fn(),
deleteThreePid: jest.fn(),
});
mockClient.getCapabilities.mockResolvedValue({});
mockClient.getThreePids.mockResolvedValue({
threepids: [],
});
mockClient.deleteThreePid.mockResolvedValue({
id_server_unbind_result: "success",
});
OIDC: use delegated auth account URL from `OidcClientStore` (#11723) * test persistCredentials without a pickle key * test setLoggedIn with pickle key * lint * type error * extract token persisting code into function, persist refresh token * store has_refresh_token too * pass refreshToken from oidcAuthGrant into credentials * rest restore session with pickle key * retreive stored refresh token and add to credentials * extract token decryption into function * remove TODO * very messy poc * utils to persist clientId and issuer after oidc authentication * add dep oidc-client-ts * persist issuer and clientId after successful oidc auth * add OidcClientStore * comments and tidy * expose getters for stored refresh and access tokens in Lifecycle * revoke tokens with oidc provider * test logout action in MatrixChat * comments * prettier * test OidcClientStore.revokeTokens * put pickle key destruction back * comment pedantry * working refresh without persistence * extract token persistence functions to utils * add sugar * implement TokenRefresher class with persistence * tidying * persist idTokenClaims * persist idTokenClaims * tests * remove unused cde * create token refresher during doSetLoggedIn * tidying * also tidying * OidcClientStore.initClient use stored issuer when client well known unavailable * test Lifecycle.logout * update Lifecycle test replaceUsingCreds calls * fix test * add sdkContext to UserSettingsDialog * use sdkContext and oidcClientStore in session manager * use sdkContext and OidcClientStore in generalusersettingstab * tidy * test tokenrefresher creation in login flow * test token refresher * Update src/utils/oidc/TokenRefresher.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * use literal value for m.authentication Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * improve comments * fix test mock, comment * typo * add sdkContext to SoftLogout, pass oidcClientStore to logout * fullstops * comments * fussy comment formatting --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-16 01:03:25 +02: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);
});
afterEach(() => {
jest.restoreAllMocks();
});
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";
OIDC: use delegated auth account URL from `OidcClientStore` (#11723) * test persistCredentials without a pickle key * test setLoggedIn with pickle key * lint * type error * extract token persisting code into function, persist refresh token * store has_refresh_token too * pass refreshToken from oidcAuthGrant into credentials * rest restore session with pickle key * retreive stored refresh token and add to credentials * extract token decryption into function * remove TODO * very messy poc * utils to persist clientId and issuer after oidc authentication * add dep oidc-client-ts * persist issuer and clientId after successful oidc auth * add OidcClientStore * comments and tidy * expose getters for stored refresh and access tokens in Lifecycle * revoke tokens with oidc provider * test logout action in MatrixChat * comments * prettier * test OidcClientStore.revokeTokens * put pickle key destruction back * comment pedantry * working refresh without persistence * extract token persistence functions to utils * add sugar * implement TokenRefresher class with persistence * tidying * persist idTokenClaims * persist idTokenClaims * tests * remove unused cde * create token refresher during doSetLoggedIn * tidying * also tidying * OidcClientStore.initClient use stored issuer when client well known unavailable * test Lifecycle.logout * update Lifecycle test replaceUsingCreds calls * fix test * add sdkContext to UserSettingsDialog * use sdkContext and oidcClientStore in session manager * use sdkContext and OidcClientStore in generalusersettingstab * tidy * test tokenrefresher creation in login flow * test token refresher * Update src/utils/oidc/TokenRefresher.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * use literal value for m.authentication Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * improve comments * fix test mock, comment * typo * add sdkContext to SoftLogout, pass oidcClientStore to logout * fullstops * comments * fussy comment formatting --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-16 01:03:25 +02:00
const mockOidcClientStore = {
accountManagementEndpoint: accountManagementLink,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
render(getComponent());
const manageAccountLink = await screen.findByRole("button", { name: "Manage account" });
expect(manageAccountLink.getAttribute("href")).toMatch(accountManagementLink);
});
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());
expect(screen.queryByText("Deactivate Account")).not.toBeInTheDocument();
expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.Deactivate);
});
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
OIDC: use delegated auth account URL from `OidcClientStore` (#11723) * test persistCredentials without a pickle key * test setLoggedIn with pickle key * lint * type error * extract token persisting code into function, persist refresh token * store has_refresh_token too * pass refreshToken from oidcAuthGrant into credentials * rest restore session with pickle key * retreive stored refresh token and add to credentials * extract token decryption into function * remove TODO * very messy poc * utils to persist clientId and issuer after oidc authentication * add dep oidc-client-ts * persist issuer and clientId after successful oidc auth * add OidcClientStore * comments and tidy * expose getters for stored refresh and access tokens in Lifecycle * revoke tokens with oidc provider * test logout action in MatrixChat * comments * prettier * test OidcClientStore.revokeTokens * put pickle key destruction back * comment pedantry * working refresh without persistence * extract token persistence functions to utils * add sugar * implement TokenRefresher class with persistence * tidying * persist idTokenClaims * persist idTokenClaims * tests * remove unused cde * create token refresher during doSetLoggedIn * tidying * also tidying * OidcClientStore.initClient use stored issuer when client well known unavailable * test Lifecycle.logout * update Lifecycle test replaceUsingCreds calls * fix test * add sdkContext to UserSettingsDialog * use sdkContext and oidcClientStore in session manager * use sdkContext and OidcClientStore in generalusersettingstab * tidy * test tokenrefresher creation in login flow * test token refresher * Update src/utils/oidc/TokenRefresher.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * use literal value for m.authentication Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * improve comments * fix test mock, comment * typo * add sdkContext to SoftLogout, pass oidcClientStore to logout * fullstops * comments * fussy comment formatting --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-16 01:03:25 +02:00
const accountManagementLink = "https://id.server.org/my-account";
const mockOidcClientStore = {
accountManagementEndpoint: accountManagementLink,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
render(getComponent());
await flushPromises();
expect(screen.queryByText("Deactivate Account")).not.toBeInTheDocument();
});
it("should render section when account deactivation feature is enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === UIFeature.Deactivate,
);
render(getComponent());
expect(screen.getByText("Deactivate Account", { selector: "h2" }).parentElement!).toMatchSnapshot();
});
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();
});
});
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();
});
});
});
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,
});
});
});
});