aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/account/profile
diff options
context:
space:
mode:
authorBenoit <benoit.gianinetti@sonarsource.com>2019-07-12 14:06:47 +0200
committerSonarTech <sonartech@sonarsource.com>2019-07-12 20:21:16 +0200
commit7f1afd8ce4723dad04762837efec3b4b2525dff5 (patch)
tree868fce46d90be48623e237c195a64e9aff091696 /server/sonar-web/src/main/js/apps/account/profile
parentc2d9ced3637a5aa08422427e6435aaecaf659feb (diff)
downloadsonarqube-7f1afd8ce4723dad04762837efec3b4b2525dff5.tar.gz
sonarqube-7f1afd8ce4723dad04762837efec3b4b2525dff5.zip
MMF-769 User can close their account (#1861)
Diffstat (limited to 'server/sonar-web/src/main/js/apps/account/profile')
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/Profile.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx125
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx148
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx150
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx71
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx86
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap118
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap128
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap19
10 files changed, 941 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx b/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
index b752f8fa8b3..941371b710e 100644
--- a/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
+++ b/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import UserExternalIdentity from './UserExternalIdentity';
+import UserDeleteAccount from './UserDeleteAccount';
import UserGroups from './UserGroups';
import UserScmAccounts from './UserScmAccounts';
import { isSonarCloud } from '../../../helpers/system';
@@ -59,6 +60,14 @@ export function Profile({ currentUser }: Props) {
<hr />
<UserScmAccounts scmAccounts={currentUser.scmAccounts} user={currentUser} />
+
+ {isSonarCloud() && (
+ <>
+ <hr />
+
+ <UserDeleteAccount user={currentUser} />
+ </>
+ )}
</div>
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx
new file mode 100644
index 00000000000..38351f002b4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import UserDeleteAccountModal from './UserDeleteAccountModal';
+import UserDeleteAccountContent from './UserDeleteAccountContent';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
+import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations';
+import { getOrganizationsThatPreventDeletion } from '../../../api/organizations';
+
+interface Props {
+ user: T.LoggedInUser;
+ userOrganizations: T.Organization[];
+}
+
+interface State {
+ loading: boolean;
+ organizationsToTransferOrDelete: T.Organization[];
+ showModal: boolean;
+}
+
+export class UserDeleteAccount extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ state: State = {
+ loading: true,
+ organizationsToTransferOrDelete: [],
+ showModal: false
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchOrganizationsThatPreventDeletion();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchOrganizationsThatPreventDeletion = () => {
+ getOrganizationsThatPreventDeletion().then(
+ ({ organizations }) => {
+ if (this.mounted) {
+ this.setState({
+ loading: false,
+ organizationsToTransferOrDelete: organizations
+ });
+ }
+ },
+ () => {}
+ );
+ };
+
+ toggleModal = () => {
+ if (this.mounted) {
+ this.setState(state => ({
+ showModal: !state.showModal
+ }));
+ }
+ };
+
+ render() {
+ const { user, userOrganizations } = this.props;
+ const { organizationsToTransferOrDelete, loading, showModal } = this.state;
+
+ const label = translate('my_profile.delete_account');
+
+ return (
+ <div>
+ <h2 className="spacer-bottom">{label}</h2>
+
+ <DeferredSpinner loading={loading} />
+
+ {!loading && (
+ <>
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-top big-spacer-bottom"
+ organizationsSafeToDelete={userOrganizations}
+ organizationsToTransferOrDelete={organizationsToTransferOrDelete}
+ />
+
+ <Button
+ className="button-red"
+ disabled={organizationsToTransferOrDelete.length > 0}
+ onClick={this.toggleModal}
+ type="button">
+ {translate('delete')}
+ </Button>
+
+ {showModal && (
+ <UserDeleteAccountModal
+ label={label}
+ organizationsSafeToDelete={userOrganizations}
+ organizationsToTransferOrDelete={organizationsToTransferOrDelete}
+ toggleModal={this.toggleModal}
+ user={user}
+ />
+ )}
+ </>
+ )}
+ </div>
+ );
+ }
+}
+
+export default whenLoggedIn(withUserOrganizations(UserDeleteAccount));
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx
new file mode 100644
index 00000000000..aaad598307b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getOrganizationUrl } from '../../../helpers/urls';
+
+function getOrganizationLink(org: T.Organization, i: number, organizations: T.Organization[]) {
+ return (
+ <span key={org.key}>
+ <Link to={getOrganizationUrl(org.key)}>{org.name}</Link>
+ {i < organizations.length - 1 && ', '}
+ </span>
+ );
+}
+
+export function ShowOrganizationsToTransferOrDelete({
+ organizations
+}: {
+ organizations: T.Organization[];
+}) {
+ return (
+ <>
+ <p className="big-spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.info.orgs_to_transfer_or_delete')}
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete"
+ values={{
+ organizations: <>{organizations.map(getOrganizationLink)}</>
+ }}
+ />
+ </p>
+
+ <Alert className="big-spacer-bottom" variant="warning">
+ <FormattedMessage
+ defaultMessage={translate(
+ 'my_profile.delete_account.info.orgs_to_transfer_or_delete.info'
+ )}
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
+ values={{
+ link: (
+ <a
+ href="https://sieg.eu.ngrok.io/documentation/organizations/overview/#how-to-transfer-ownership-of-an-organization"
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate('my_profile.delete_account.info.orgs_to_transfer_or_delete.info.link')}
+ </a>
+ )
+ }}
+ />
+ </Alert>
+ </>
+ );
+}
+
+export function ShowOrganizations({
+ className,
+ organizations
+}: {
+ className?: string;
+ organizations: T.Organization[];
+}) {
+ const organizationsIAdministrate = organizations.filter(o => o.actions && o.actions.admin);
+
+ return (
+ <ul className={className}>
+ <li className="spacer-bottom">{translate('my_profile.delete_account.info')}</li>
+
+ <li className="spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.data.info')}
+ id="my_profile.delete_account.data.info"
+ values={{
+ help: (
+ <a
+ href="/documentation/user-guide/user-account/#delete-your-user-account"
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate('learn_more')}
+ </a>
+ )
+ }}
+ />
+ </li>
+
+ {organizations.length > 0 && (
+ <li className="spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.info.orgs.members')}
+ id="my_profile.delete_account.info.orgs.members"
+ values={{
+ organizations: <>{organizations.map(getOrganizationLink)}</>
+ }}
+ />
+ </li>
+ )}
+
+ {organizationsIAdministrate.length > 0 && (
+ <li className="spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('my_profile.delete_account.info.orgs.administrators')}
+ id="my_profile.delete_account.info.orgs.administrators"
+ values={{
+ organizations: <>{organizationsIAdministrate.map(getOrganizationLink)}</>
+ }}
+ />
+ </li>
+ )}
+ </ul>
+ );
+}
+
+interface UserDeleteAccountContentProps {
+ className?: string;
+ organizationsSafeToDelete: T.Organization[];
+ organizationsToTransferOrDelete: T.Organization[];
+}
+
+export default function UserDeleteAccountContent({
+ className,
+ organizationsSafeToDelete,
+ organizationsToTransferOrDelete
+}: UserDeleteAccountContentProps) {
+ if (organizationsToTransferOrDelete.length > 0) {
+ return <ShowOrganizationsToTransferOrDelete organizations={organizationsToTransferOrDelete} />;
+ }
+
+ return <ShowOrganizations className={className} organizations={organizationsSafeToDelete} />;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx
new file mode 100644
index 00000000000..566fad469f6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx
@@ -0,0 +1,150 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormikProps } from 'formik';
+import { connect } from 'react-redux';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import InputValidationField from 'sonar-ui-common/components/controls/InputValidationField';
+import UserDeleteAccountContent from './UserDeleteAccountContent';
+import RecentHistory from '../../../app/components/RecentHistory';
+import ValidationModal from '../../../components/controls/ValidationModal';
+import { deactivateUser } from '../../../api/users';
+import { Router, withRouter } from '../../../components/hoc/withRouter';
+import { doLogout } from '../../../store/rootActions';
+
+interface Values {
+ login: string;
+}
+
+interface DeleteModalProps {
+ doLogout: () => Promise<void>;
+ label: string;
+ organizationsSafeToDelete: T.Organization[];
+ organizationsToTransferOrDelete: T.Organization[];
+ router: Pick<Router, 'push'>;
+ toggleModal: VoidFunction;
+ user: T.LoggedInUser;
+}
+
+export class UserDeleteAccountModal extends React.PureComponent<DeleteModalProps> {
+ handleSubmit = () => {
+ const { user } = this.props;
+
+ return deactivateUser({ login: user.login })
+ .then(this.props.doLogout)
+ .then(() => {
+ RecentHistory.clear();
+ window.location.replace('/account-deleted');
+ });
+ };
+
+ handleValidate = ({ login }: Values) => {
+ const { user } = this.props;
+ const errors: { login?: string } = {};
+ const trimmedLogin = login.trim();
+
+ if (!trimmedLogin) {
+ errors.login = translate('my_profile.delete_account.login.required');
+ } else if (user.externalIdentity && trimmedLogin !== user.externalIdentity.trim()) {
+ errors.login = translate('my_profile.delete_account.login.wrong_value');
+ }
+
+ return errors;
+ };
+
+ render() {
+ const {
+ label,
+ organizationsSafeToDelete,
+ organizationsToTransferOrDelete,
+ toggleModal,
+ user
+ } = this.props;
+
+ return (
+ <ValidationModal
+ confirmButtonText={translate('delete')}
+ header={translateWithParameters(
+ 'my_profile.delete_account.modal.header',
+ label,
+ user.externalIdentity || ''
+ )}
+ initialValues={{
+ login: ''
+ }}
+ isDestructive={true}
+ onClose={toggleModal}
+ onSubmit={this.handleSubmit}
+ validate={this.handleValidate}>
+ {({
+ dirty,
+ errors,
+ handleBlur,
+ handleChange,
+ isSubmitting,
+ touched,
+ values
+ }: FormikProps<Values>) => (
+ <>
+ <Alert className="big-spacer-bottom" variant="error">
+ {translate('my_profile.warning_message')}
+ </Alert>
+
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-bottom"
+ organizationsSafeToDelete={organizationsSafeToDelete}
+ organizationsToTransferOrDelete={organizationsToTransferOrDelete}
+ />
+
+ <InputValidationField
+ autoFocus={true}
+ dirty={dirty}
+ disabled={isSubmitting}
+ error={errors.login}
+ id="user-login"
+ label={
+ <label htmlFor="user-login">
+ {translate('my_profile.delete_account.verify')}
+ <em className="mandatory">*</em>
+ </label>
+ }
+ name="login"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ touched={touched.login}
+ type="text"
+ value={values.login}
+ />
+ </>
+ )}
+ </ValidationModal>
+ );
+ }
+}
+
+const mapStateToProps = () => ({});
+
+const mapDispatchToProps = { doLogout: doLogout as any };
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(withRouter(UserDeleteAccountModal));
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx
new file mode 100644
index 00000000000..1e4d4b217e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { mockLoggedInUser, mockOrganization } from '../../../../helpers/testMocks';
+import { UserDeleteAccount } from '../UserDeleteAccount';
+import { getOrganizationsThatPreventDeletion } from '../../../../api/organizations';
+
+jest.mock('../../../../api/organizations', () => ({
+ getOrganizationsThatPreventDeletion: jest.fn().mockResolvedValue({ organizations: [] })
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+const organizationToTransferOrDelete = {
+ key: 'luke-leia',
+ name: 'Luke and Leia'
+};
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper.find('Button'));
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should get some organizations', async () => {
+ (getOrganizationsThatPreventDeletion as jest.Mock).mockResolvedValue({
+ organizations: [organizationToTransferOrDelete]
+ });
+
+ const wrapper = shallowRender();
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state('loading')).toBeFalsy();
+ expect(wrapper.state('organizationsToTransferOrDelete')).toEqual([
+ organizationToTransferOrDelete
+ ]);
+ expect(getOrganizationsThatPreventDeletion).toBeCalled();
+ expect(wrapper.find('Button').prop('disabled')).toBe(true);
+});
+
+it('should toggle modal', () => {
+ const wrapper = shallowRender();
+ wrapper.setState({ loading: false });
+ expect(wrapper.find('Connect(withRouter(UserDeleteAccountModal))').exists()).toBe(false);
+ click(wrapper.find('Button'));
+ expect(wrapper.find('Connect(withRouter(UserDeleteAccountModal))').exists()).toBe(true);
+});
+
+function shallowRender(props: Partial<UserDeleteAccount['props']> = {}) {
+ const user = mockLoggedInUser({ externalIdentity: 'luke' });
+
+ const userOrganizations = [
+ mockOrganization({ key: 'luke-leia', name: 'Luke and Leia' }),
+ mockOrganization({ key: 'luke', name: 'Luke Skywalker' })
+ ];
+
+ return shallow<UserDeleteAccount>(
+ <UserDeleteAccount user={user} userOrganizations={userOrganizations} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx
new file mode 100644
index 00000000000..c9e5e334d5e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import UserDeleteAccountContent, {
+ ShowOrganizations,
+ ShowOrganizationsToTransferOrDelete
+} from '../UserDeleteAccountContent';
+
+const organizationSafeToDelete = {
+ key: 'luke',
+ name: 'Luke Skywalker'
+};
+
+const organizationToTransferOrDelete = {
+ key: 'luke-leia',
+ name: 'Luke and Leia'
+};
+
+it('should render content correctly', () => {
+ expect(
+ shallow(
+ <UserDeleteAccountContent
+ className="my-class"
+ organizationsSafeToDelete={[organizationSafeToDelete]}
+ organizationsToTransferOrDelete={[organizationToTransferOrDelete]}
+ />
+ )
+ ).toMatchSnapshot();
+
+ expect(
+ shallow(
+ <UserDeleteAccountContent
+ className="my-class"
+ organizationsSafeToDelete={[organizationSafeToDelete]}
+ organizationsToTransferOrDelete={[]}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render correctly ShowOrganizationsToTransferOrDelete', () => {
+ expect(
+ shallow(
+ <ShowOrganizationsToTransferOrDelete organizations={[organizationToTransferOrDelete]} />
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render correctly ShowOrganizations', () => {
+ expect(
+ shallow(<ShowOrganizations organizations={[organizationSafeToDelete]} />)
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx
new file mode 100644
index 00000000000..e820b6c0284
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
+import { UserDeleteAccountModal } from '../UserDeleteAccountModal';
+import { deactivateUser } from '../../../../api/users';
+
+jest.mock('../../../../api/users', () => ({
+ deactivateUser: jest.fn()
+}));
+
+const organizationSafeToDelete = {
+ key: 'luke',
+ name: 'Luke Skywalker'
+};
+
+const organizationToTransferOrDelete = {
+ key: 'luke-leia',
+ name: 'Luke and Leia'
+};
+
+it('should render modal correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should handle submit', async () => {
+ (deactivateUser as jest.Mock).mockResolvedValue(true);
+ window.location.replace = jest.fn();
+
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+
+ instance.handleSubmit();
+ await waitAndUpdate(wrapper);
+
+ expect(deactivateUser).toBeCalled();
+ expect(window.location.replace).toHaveBeenCalledWith('/account-deleted');
+});
+
+it('should validate user input', () => {
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+ const { handleValidate } = instance;
+
+ expect(handleValidate({ login: '' }).login).toBe('my_profile.delete_account.login.required');
+ expect(handleValidate({ login: 'abc' }).login).toBe(
+ 'my_profile.delete_account.login.wrong_value'
+ );
+ expect(handleValidate({ login: 'luke' }).login).toBeUndefined();
+});
+
+function shallowRender(props: Partial<UserDeleteAccountModal['props']> = {}) {
+ const user = mockLoggedInUser({ externalIdentity: 'luke' });
+
+ return shallow<UserDeleteAccountModal>(
+ <UserDeleteAccountModal
+ doLogout={jest.fn().mockResolvedValue(true)}
+ label="label"
+ organizationsSafeToDelete={[organizationSafeToDelete]}
+ organizationsToTransferOrDelete={[organizationToTransferOrDelete]}
+ router={mockRouter()}
+ toggleModal={jest.fn()}
+ user={user}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap
new file mode 100644
index 00000000000..05a4397a565
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap
@@ -0,0 +1,118 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div>
+ <h2
+ className="spacer-bottom"
+ >
+ my_profile.delete_account
+ </h2>
+ <DeferredSpinner
+ loading={true}
+ timeout={100}
+ />
+</div>
+`;
+
+exports[`should render correctly 2`] = `
+<div>
+ <h2
+ className="spacer-bottom"
+ >
+ my_profile.delete_account
+ </h2>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-top big-spacer-bottom"
+ organizationsSafeToDelete={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+ organizationsToTransferOrDelete={Array []}
+ />
+ <Button
+ className="button-red"
+ disabled={false}
+ onClick={[Function]}
+ type="button"
+ >
+ delete
+ </Button>
+</div>
+`;
+
+exports[`should render correctly 3`] = `
+<div>
+ <h2
+ className="spacer-bottom"
+ >
+ my_profile.delete_account
+ </h2>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <UserDeleteAccountContent
+ className="list-styled no-padding big-spacer-top big-spacer-bottom"
+ organizationsSafeToDelete={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+ organizationsToTransferOrDelete={Array []}
+ />
+ <Button
+ className="button-red"
+ disabled={false}
+ onClick={[Function]}
+ type="button"
+ >
+ delete
+ </Button>
+ <Connect(withRouter(UserDeleteAccountModal))
+ label="my_profile.delete_account"
+ organizationsSafeToDelete={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+ organizationsToTransferOrDelete={Array []}
+ toggleModal={[Function]}
+ user={
+ Object {
+ "externalIdentity": "luke",
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "luke",
+ "name": "Skywalker",
+ "scmAccounts": Array [],
+ }
+ }
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap
new file mode 100644
index 00000000000..56ac708fb53
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render content correctly 1`] = `
+<ShowOrganizationsToTransferOrDelete
+ organizations={
+ Array [
+ Object {
+ "key": "luke-leia",
+ "name": "Luke and Leia",
+ },
+ ]
+ }
+/>
+`;
+
+exports[`should render content correctly 2`] = `
+<ShowOrganizations
+ className="my-class"
+ organizations={
+ Array [
+ Object {
+ "key": "luke",
+ "name": "Luke Skywalker",
+ },
+ ]
+ }
+/>
+`;
+
+exports[`should render correctly ShowOrganizations 1`] = `
+<ul>
+ <li
+ className="spacer-bottom"
+ >
+ my_profile.delete_account.info
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.data.info"
+ id="my_profile.delete_account.data.info"
+ values={
+ Object {
+ "help": <a
+ href="/documentation/user-guide/user-account/#delete-your-user-account"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ learn_more
+ </a>,
+ }
+ }
+ />
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.info.orgs.members"
+ id="my_profile.delete_account.info.orgs.members"
+ values={
+ Object {
+ "organizations": <React.Fragment>
+ <span>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/luke"
+ >
+ Luke Skywalker
+ </Link>
+ </span>
+ </React.Fragment>,
+ }
+ }
+ />
+ </li>
+</ul>
+`;
+
+exports[`should render correctly ShowOrganizationsToTransferOrDelete 1`] = `
+<Fragment>
+ <p
+ className="big-spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.info.orgs_to_transfer_or_delete"
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete"
+ values={
+ Object {
+ "organizations": <React.Fragment>
+ <span>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/luke-leia"
+ >
+ Luke and Leia
+ </Link>
+ </span>
+ </React.Fragment>,
+ }
+ }
+ />
+ </p>
+ <Alert
+ className="big-spacer-bottom"
+ variant="warning"
+ >
+ <FormattedMessage
+ defaultMessage="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
+ id="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
+ values={
+ Object {
+ "link": <a
+ href="https://sieg.eu.ngrok.io/documentation/organizations/overview/#how-to-transfer-ownership-of-an-organization"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ my_profile.delete_account.info.orgs_to_transfer_or_delete.info.link
+ </a>,
+ }
+ }
+ />
+ </Alert>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap
new file mode 100644
index 00000000000..5ba1d10e4ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render modal correctly 1`] = `
+<ValidationModal
+ confirmButtonText="delete"
+ header="my_profile.delete_account.modal.header.label.luke"
+ initialValues={
+ Object {
+ "login": "",
+ }
+ }
+ isDestructive={true}
+ onClose={[MockFunction]}
+ onSubmit={[Function]}
+ validate={[Function]}
+>
+ <Component />
+</ValidationModal>
+`;