]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18890 Users and Groups pages migrated to RTL
authorvikvorona <viktor.vorona@sonarsource.com>
Fri, 5 May 2023 10:15:20 +0000 (12:15 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 5 May 2023 20:03:00 +0000 (20:03 +0000)
32 files changed:
server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts
server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts
server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts
server/sonar-web/src/main/js/api/users.ts
server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/GroupsApp.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/groups/routes.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/DeactivateForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/PasswordForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormModal-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/PasswordForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserGroups-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/helpers/testMocks.ts

index fcf3191a99ec32be87630b9f359a4b9fb4855669..523f59c2237d5e303943acba8ddc35fdea99be7d 100644 (file)
@@ -24,7 +24,7 @@ import {
   mockGroup,
   mockIdentityProvider,
   mockPaging,
-  mockUser,
+  mockUserGroupMember,
 } from '../../helpers/testMocks';
 import {
   Group,
@@ -34,24 +34,37 @@ import {
   UserGroupMember,
 } from '../../types/types';
 import { getSystemInfo } from '../system';
-import { getIdentityProviders } from '../users';
 import {
+  addUserToGroup,
   createGroup,
   deleteGroup,
   getUsersInGroup,
+  removeUserFromGroup,
   searchUsersGroups,
   updateGroup,
 } from '../user_groups';
+import { getIdentityProviders } from '../users';
+
+jest.mock('../users');
+jest.mock('../system');
+jest.mock('../user_groups');
 
 export default class GroupsServiceMock {
   isManaged = false;
   paging: Paging;
   groups: Group[];
+  users: UserGroupMember[];
   readOnlyGroups = [
     mockGroup({ name: 'managed-group', managed: true }),
     mockGroup({ name: 'local-group', managed: false }),
   ];
 
+  defaultUsers = [
+    mockUserGroupMember({ name: 'alice', login: 'alice.dev' }),
+    mockUserGroupMember({ name: 'bob', login: 'bob.dev' }),
+    mockUserGroupMember({ selected: false }),
+  ];
+
   constructor() {
     this.groups = cloneDeep(this.readOnlyGroups);
     this.paging = mockPaging({
@@ -59,6 +72,7 @@ export default class GroupsServiceMock {
       pageSize: 2,
       total: 200,
     });
+    this.users = cloneDeep(this.defaultUsers);
 
     jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
     jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders);
@@ -67,10 +81,13 @@ export default class GroupsServiceMock {
     jest.mocked(deleteGroup).mockImplementation((g) => this.handleDeleteGroup(g));
     jest.mocked(updateGroup).mockImplementation((g) => this.handleUpdateGroup(g));
     jest.mocked(getUsersInGroup).mockImplementation(this.handlegetUsersInGroup);
+    jest.mocked(addUserToGroup).mockImplementation(this.handleAddUserToGroup);
+    jest.mocked(removeUserFromGroup).mockImplementation(this.handleRemoveUserFromGroup);
   }
 
   reset() {
     this.groups = cloneDeep(this.readOnlyGroups);
+    this.users = cloneDeep(this.defaultUsers);
   }
 
   setIsManaged(managed: boolean) {
@@ -132,16 +149,18 @@ export default class GroupsServiceMock {
   }): Promise<Paging & { users: UserGroupMember[] }> => {
     return this.reply({
       ...this.paging,
-      users: [
-        {
-          ...mockUser({ name: 'alice' }),
-          selected: true,
-        } as UserGroupMember,
-        {
-          ...mockUser({ name: 'bob' }),
-          selected: false,
-        } as UserGroupMember,
-      ].filter((u) => u.name.includes(data.q ?? '')),
+      users: this.users
+        .filter((u) => u.name.includes(data.q ?? ''))
+        .filter((u) => {
+          switch (data.selected) {
+            case 'selected':
+              return u.selected;
+            case 'deselected':
+              return !u.selected;
+            default:
+              return true;
+          }
+        }),
     });
   };
 
@@ -201,6 +220,16 @@ export default class GroupsServiceMock {
     );
   };
 
+  handleAddUserToGroup: typeof addUserToGroup = ({ login }) => {
+    this.users = this.users.map((u) => (u.login === login ? { ...u, selected: true } : u));
+    return this.reply({});
+  };
+
+  handleRemoveUserFromGroup: typeof removeUserFromGroup = ({ login }) => {
+    this.users = this.users.map((u) => (u.login === login ? { ...u, selected: false } : u));
+    return this.reply({});
+  };
+
   reply<T>(response: T): Promise<T> {
     return Promise.resolve(cloneDeep(response));
   }
index 4c6de69c5d9512f80a3c11d1f035169e27609629..f0f9f523a9e5d39748cd15a201c9adf79811c272 100644 (file)
@@ -68,6 +68,10 @@ export default class UserTokensMock {
       return Promise.reject('x_x');
     }
 
+    if (this.tokens.some((t) => t.name === name)) {
+      return Promise.reject('This name is already used');
+    }
+
     const token = {
       name,
       login,
@@ -85,13 +89,7 @@ export default class UserTokensMock {
   };
 
   handleRevokeToken = ({ name }: { name: string; login?: string }) => {
-    const index = this.tokens.findIndex((t) => t.name === name);
-
-    if (index < 0) {
-      return Promise.resolve();
-    }
-
-    this.tokens.splice(index, 1);
+    this.tokens = this.tokens.filter((t) => t.name !== name);
 
     return Promise.resolve();
   };
index a46e8bb94bf3d48e9167f839966bd8fe5a91ef92..020683d6171366060879141fd3b4f97ef9921305 100644 (file)
  */
 
 import { isAfter, isBefore } from 'date-fns';
-import { cloneDeep } from 'lodash';
+import { cloneDeep, isEmpty, isUndefined, omitBy } from 'lodash';
 import { mockClusterSysInfo, mockIdentityProvider, mockUser } from '../../helpers/testMocks';
 import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types';
-import { User } from '../../types/users';
+import { ChangePasswordResults, User } from '../../types/users';
 import { getSystemInfo } from '../system';
-import { createUser, getIdentityProviders, searchUsers } from '../users';
+import { addUserToGroup, removeUserFromGroup } from '../user_groups';
+import {
+  UserGroup,
+  changePassword,
+  createUser,
+  deactivateUser,
+  getIdentityProviders,
+  getUserGroups,
+  searchUsers,
+  updateUser,
+} from '../users';
+
+jest.mock('../users');
+jest.mock('../user_groups');
+jest.mock('../system');
 
 const DEFAULT_USERS = [
   mockUser({
@@ -40,16 +54,23 @@ const DEFAULT_USERS = [
     name: 'Alice Merveille',
     lastConnectionDate: '2023-06-27T17:08:59+0200',
     sonarLintLastConnectionDate: '2023-05-27T17:08:59+0200',
+    groups: ['group1', 'group2', 'group3', 'group4'],
   }),
   mockUser({
     managed: false,
+    local: false,
     login: 'charlie.cox',
     name: 'Charlie Cox',
     lastConnectionDate: '2023-06-25T17:08:59+0200',
     sonarLintLastConnectionDate: '2023-06-20T12:10:59+0200',
+    externalProvider: 'test',
+    externalIdentity: 'ExternalTest',
   }),
   mockUser({
     managed: true,
+    local: false,
+    externalProvider: 'test2',
+    externalIdentity: 'UnknownExternalProvider',
     login: 'denis.villeneuve',
     name: 'Denis Villeneuve',
     lastConnectionDate: '2023-06-20T15:08:59+0200',
@@ -68,14 +89,48 @@ const DEFAULT_USERS = [
   }),
 ];
 
+const DEFAULT_GROUPS: UserGroup[] = [
+  {
+    id: 1001,
+    name: 'test1',
+    description: 'test1',
+    selected: true,
+    default: true,
+  },
+  {
+    id: 1002,
+    name: 'test2',
+    description: 'test2',
+    selected: true,
+    default: false,
+  },
+  {
+    id: 1003,
+    name: 'test3',
+    description: 'test3',
+    selected: false,
+    default: false,
+  },
+];
+
+const DEFAULT_PASSWORD = 'test';
+
 export default class UsersServiceMock {
   isManaged = true;
   users = cloneDeep(DEFAULT_USERS);
+  groups = cloneDeep(DEFAULT_GROUPS);
+  password = DEFAULT_PASSWORD;
   constructor() {
     jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
     jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders);
     jest.mocked(searchUsers).mockImplementation((p) => this.handleSearchUsers(p));
     jest.mocked(createUser).mockImplementation(this.handleCreateUser);
+    jest.mocked(updateUser).mockImplementation(this.handleUpdateUser);
+    jest.mocked(getUserGroups).mockImplementation(this.handleGetUserGroups);
+    jest.mocked(addUserToGroup).mockImplementation(this.handleAddUserToGroup);
+    jest.mocked(removeUserFromGroup).mockImplementation(this.handleRemoveUserFromGroup);
+    jest.mocked(changePassword).mockImplementation(this.handleChangePassword);
+    jest.mocked(deactivateUser).mockImplementation(this.handleDeactivateUser);
   }
 
   setIsManaged(managed: boolean) {
@@ -187,6 +242,12 @@ export default class UsersServiceMock {
     scmAccount: string[];
   }) => {
     const { email, local, login, name, scmAccount } = data;
+    if (scmAccount.some((a) => isEmpty(a.trim()))) {
+      return Promise.reject({
+        status: 400,
+        json: () => Promise.resolve({ errors: [{ msg: 'Error: Empty SCM' }] }),
+      });
+    }
     const newUser = mockUser({
       email,
       local,
@@ -198,8 +259,27 @@ export default class UsersServiceMock {
     return this.reply(undefined);
   };
 
+  handleUpdateUser = (data: {
+    email?: string;
+    login: string;
+    name: string;
+    scmAccount: string[];
+  }) => {
+    const { email, login, name, scmAccount } = data;
+    const user = this.users.find((u) => u.login === login);
+    if (!user) {
+      return Promise.reject('No such user');
+    }
+    Object.assign(user, {
+      ...omitBy({ name, email, scmAccount }, isUndefined),
+    });
+    return this.reply({ user });
+  };
+
   handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => {
-    return this.reply({ identityProviders: [mockIdentityProvider()] });
+    return this.reply({
+      identityProviders: [mockIdentityProvider({ key: 'test' })],
+    });
   };
 
   handleGetSystemInfo = (): Promise<SysInfoCluster> => {
@@ -218,9 +298,72 @@ export default class UsersServiceMock {
     );
   };
 
+  handleGetUserGroups: typeof getUserGroups = (data) => {
+    const filteredGroups = this.groups
+      .filter((g) => g.name.includes(data.q ?? ''))
+      .filter((g) => {
+        switch (data.selected) {
+          case 'selected':
+            return g.selected;
+          case 'deselected':
+            return !g.selected;
+          default:
+            return true;
+        }
+      });
+    return this.reply({
+      paging: { pageIndex: 1, pageSize: 10, total: filteredGroups.length },
+      groups: filteredGroups,
+    });
+  };
+
+  handleAddUserToGroup: typeof addUserToGroup = ({ name }) => {
+    this.groups = this.groups.map((g) => (g.name === name ? { ...g, selected: true } : g));
+    return this.reply({});
+  };
+
+  handleRemoveUserFromGroup: typeof removeUserFromGroup = ({ name }) => {
+    let isDefault = false;
+    this.groups = this.groups.map((g) => {
+      if (g.name === name) {
+        if (g.default) {
+          isDefault = true;
+          return g;
+        }
+        return { ...g, selected: false };
+      }
+      return g;
+    });
+    return isDefault
+      ? Promise.reject({
+          errors: [{ msg: 'Cannot remove Default group' }],
+        })
+      : this.reply({});
+  };
+
+  handleChangePassword: typeof changePassword = (data) => {
+    if (data.previousPassword !== this.password) {
+      return Promise.reject(ChangePasswordResults.OldPasswordIncorrect);
+    }
+    if (data.password === this.password) {
+      return Promise.reject(ChangePasswordResults.NewPasswordSameAsOld);
+    }
+    this.password = data.password;
+    return this.reply({});
+  };
+
+  handleDeactivateUser: typeof deactivateUser = (data) => {
+    const index = this.users.findIndex((u) => u.login === data.login);
+    const user = this.users.splice(index, 1)[0];
+    user.active = false;
+    return this.reply({ user });
+  };
+
   reset = () => {
     this.isManaged = true;
     this.users = cloneDeep(DEFAULT_USERS);
+    this.groups = cloneDeep(DEFAULT_GROUPS);
+    this.password = DEFAULT_PASSWORD;
   };
 
   reply<T>(response: T): Promise<T> {
index 444092043f7c86e6959082657eae11b2a1994248..5afe097a7bb3960abf0466a8f58aa7fd107dd782 100644 (file)
@@ -97,14 +97,17 @@ export function updateUser(data: {
   login: string;
   name?: string;
   scmAccount: string[];
-}): Promise<User> {
+}): Promise<{ user: User }> {
   return postJSON('/api/users/update', {
     ...data,
     scmAccount: data.scmAccount.length > 0 ? data.scmAccount : '',
   });
 }
 
-export function deactivateUser(data: { login: string; anonymize?: boolean }): Promise<User> {
+export function deactivateUser(data: {
+  login: string;
+  anonymize?: boolean;
+}): Promise<{ user: User }> {
   return postJSON('/api/users/deactivate', data).catch(throwGlobalError);
 }
 
diff --git a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
new file mode 100644 (file)
index 0000000..50aeba4
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useCallback, useEffect, useState } from 'react';
+import { Helmet } from 'react-helmet-async';
+import { searchUsersGroups } from '../../api/user_groups';
+import ListFooter from '../../components/controls/ListFooter';
+import { ManagedFilter } from '../../components/controls/ManagedFilter';
+import SearchBox from '../../components/controls/SearchBox';
+import Suggestions from '../../components/embed-docs-modal/Suggestions';
+import { useManageProvider } from '../../components/hooks/useManageProvider';
+import { translate } from '../../helpers/l10n';
+import { Group, Paging } from '../../types/types';
+import Header from './components/Header';
+import List from './components/List';
+import './groups.css';
+
+export default function App() {
+  const [loading, setLoading] = useState<boolean>(true);
+  const [paging, setPaging] = useState<Paging>();
+  const [search, setSearch] = useState<string>('');
+  const [groups, setGroups] = useState<Group[]>([]);
+  const [managed, setManaged] = useState<boolean | undefined>();
+  const manageProvider = useManageProvider();
+
+  const fetchGroups = useCallback(async () => {
+    setLoading(true);
+    try {
+      const { groups, paging } = await searchUsersGroups({
+        q: search,
+        managed,
+      });
+      setGroups(groups);
+      setPaging(paging);
+    } finally {
+      setLoading(false);
+    }
+  }, [search, managed]);
+
+  const fetchMoreGroups = useCallback(async () => {
+    if (!paging) {
+      return;
+    }
+    setLoading(true);
+    try {
+      const { groups: nextGroups, paging: nextPage } = await searchUsersGroups({
+        q: search,
+        managed,
+        p: paging.pageIndex + 1,
+      });
+      setPaging(nextPage);
+      setGroups([...groups, ...nextGroups]);
+    } finally {
+      setLoading(false);
+    }
+  }, [groups, search, managed, paging]);
+
+  useEffect(() => {
+    fetchGroups();
+  }, [search, managed]);
+
+  return (
+    <>
+      <Suggestions suggestions="user_groups" />
+      <Helmet defer={false} title={translate('user_groups.page')} />
+      <main className="page page-limited" id="groups-page">
+        <Header reload={fetchGroups} manageProvider={manageProvider} />
+
+        <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
+          <ManagedFilter
+            manageProvider={manageProvider}
+            loading={loading}
+            managed={managed}
+            setManaged={setManaged}
+          />
+          <SearchBox
+            id="groups-search"
+            minLength={2}
+            onChange={(q) => setSearch(q)}
+            placeholder={translate('search.search_by_name')}
+            value={search}
+          />
+        </div>
+
+        <List groups={groups} reload={fetchGroups} manageProvider={manageProvider} />
+
+        {paging !== undefined && (
+          <div id="groups-list-footer">
+            <ListFooter
+              count={groups.length}
+              loading={loading}
+              loadMore={fetchMoreGroups}
+              ready={!loading}
+              total={paging.total}
+            />
+          </div>
+        )}
+      </main>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
new file mode 100644 (file)
index 0000000..6ef453b
--- /dev/null
@@ -0,0 +1,320 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { act } from 'react-dom/test-utils';
+import { byRole, byText } from 'testing-library-selector';
+import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock';
+import { renderApp } from '../../../helpers/testReactTestingUtils';
+import App from '../GroupsApp';
+
+const handler = new GroupsServiceMock();
+
+const ui = {
+  createGroupButton: byRole('button', { name: 'groups.create_group' }),
+  infoManageMode: byText(/groups\.page\.managed_description/),
+  description: byText('user_groups.page.description'),
+  allFilter: byRole('button', { name: 'all' }),
+  selectedFilter: byRole('button', { name: 'selected' }),
+  unselectedFilter: byRole('button', { name: 'unselected' }),
+  managedFilter: byRole('button', { name: 'managed' }),
+  localFilter: byRole('button', { name: 'local' }),
+  searchInput: byRole('searchbox', { name: 'search.search_by_name' }),
+  updateButton: byRole('button', { name: 'update_details' }),
+  updateDialog: byRole('dialog', { name: 'groups.update_group' }),
+  updateDialogButton: byRole('button', { name: 'update_verb' }),
+  deleteButton: byRole('button', { name: 'delete' }),
+  deleteDialog: byRole('dialog', { name: 'groups.delete_group' }),
+  deleteDialogButton: byRole('button', { name: 'delete' }),
+  showMore: byRole('button', { name: 'show_more' }),
+  nameInput: byRole('textbox', { name: 'name field_required' }),
+  descriptionInput: byRole('textbox', { name: 'description' }),
+  createGroupDialogButton: byRole('button', { name: 'create' }),
+  editGroupDialogButton: byRole('button', { name: 'groups.create_group' }),
+  reloadButton: byRole('button', { name: 'reload' }),
+  doneButton: byRole('button', { name: 'done' }),
+
+  createGroupDialog: byRole('dialog', { name: 'groups.create_group' }),
+  membersViewDialog: byRole('dialog', { name: 'users.list' }),
+  membersDialog: byRole('dialog', { name: 'users.update' }),
+  getMembers: () => within(ui.membersDialog.get()).getAllByRole('checkbox'),
+
+  managedGroupRow: byRole('row', { name: 'managed-group 1' }),
+  managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }),
+  managedGroupViewMembersButton: byRole('button', { name: 'groups.users.view.managed-group' }),
+
+  memberAliceUser: byText('alice'),
+  memberBobUser: byText('bob'),
+  memberSearchInput: byRole('searchbox', { name: 'search_verb' }),
+
+  managedEditButton: byRole('button', { name: 'groups.edit.managed-group' }),
+
+  localGroupRow: byRole('row', { name: 'local-group 1' }),
+  localGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.local-group' }),
+  localGroupRow2: byRole('row', { name: 'local-group 2 1 group 2 is loco!' }),
+  editedLocalGroupRow: byRole('row', { name: 'local-group 3 1 group 3 rocks!' }),
+  localEditButton: byRole('button', { name: 'groups.edit.local-group' }),
+  localGroupRowWithLocalBadge: byRole('row', {
+    name: 'local-group local 1',
+  }),
+};
+
+beforeEach(() => {
+  handler.reset();
+});
+
+describe('in non managed mode', () => {
+  beforeEach(() => {
+    handler.setIsManaged(false);
+  });
+
+  it('should render all groups', async () => {
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument());
+    expect(ui.managedGroupRow.get()).toBeInTheDocument();
+    expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument();
+  });
+
+  it('should be able to create a group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.description.find()).toBeInTheDocument());
+    await act(async () => {
+      await user.click(ui.createGroupButton.get());
+    });
+
+    expect(await ui.createGroupDialog.find()).toBeInTheDocument();
+
+    await act(async () => {
+      await user.type(ui.nameInput.get(), 'local-group 2');
+      await user.type(ui.descriptionInput.get(), 'group 2 is loco!');
+      await user.click(ui.createGroupDialogButton.get());
+    });
+
+    expect(await ui.localGroupRow2.find()).toBeInTheDocument();
+  });
+
+  it('should be able to delete a group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => {
+      await user.click(await ui.localEditButton.find());
+      await user.click(await ui.deleteButton.find());
+    });
+
+    expect(await ui.deleteDialog.find()).toBeInTheDocument();
+    await act(async () => {
+      await user.click(ui.deleteDialogButton.get());
+    });
+
+    expect(await ui.managedGroupRow.find()).toBeInTheDocument();
+    expect(ui.localGroupRow.query()).not.toBeInTheDocument();
+  });
+
+  it('should be able to edit a group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => {
+      await user.click(await ui.localEditButton.find());
+      await user.click(await ui.updateButton.find());
+    });
+
+    expect(ui.updateDialog.get()).toBeInTheDocument();
+
+    await act(async () => {
+      await user.clear(ui.nameInput.get());
+      await user.type(ui.nameInput.get(), 'local-group 3');
+      await user.clear(ui.descriptionInput.get());
+      await user.type(ui.descriptionInput.get(), 'group 3 rocks!');
+    });
+
+    expect(ui.updateDialog.get()).toBeInTheDocument();
+
+    await act(async () => {
+      await user.click(ui.updateDialogButton.get());
+    });
+
+    expect(await ui.managedGroupRow.find()).toBeInTheDocument();
+    expect(await ui.editedLocalGroupRow.find()).toBeInTheDocument();
+  });
+
+  it('should be able to edit the members of a group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument());
+    expect(await ui.localGroupEditMembersButton.find()).toBeInTheDocument();
+
+    await act(async () => {
+      await user.click(ui.localGroupEditMembersButton.get());
+    });
+
+    expect(await ui.membersDialog.find()).toBeInTheDocument();
+
+    expect(ui.getMembers()).toHaveLength(2);
+
+    await user.click(ui.allFilter.get());
+    expect(ui.getMembers()).toHaveLength(3);
+
+    await user.click(ui.unselectedFilter.get());
+    expect(ui.reloadButton.query()).not.toBeInTheDocument();
+    await user.click(ui.getMembers()[0]);
+    expect(await ui.reloadButton.find()).toBeInTheDocument();
+
+    await user.click(ui.selectedFilter.get());
+    expect(ui.getMembers()).toHaveLength(3);
+    expect(ui.reloadButton.query()).not.toBeInTheDocument();
+    await user.click(ui.getMembers()[0]);
+    expect(await ui.reloadButton.find()).toBeInTheDocument();
+    await user.click(ui.reloadButton.get());
+    expect(ui.getMembers()).toHaveLength(2);
+
+    await act(() => user.click(ui.doneButton.get()));
+    expect(ui.membersDialog.query()).not.toBeInTheDocument();
+  });
+
+  it('should be able search a group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument());
+    expect(ui.managedGroupRow.get()).toBeInTheDocument();
+
+    await act(async () => {
+      await user.type(await ui.searchInput.find(), 'local');
+    });
+
+    expect(await ui.localGroupRow.find()).toBeInTheDocument();
+    expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
+  });
+
+  it('should be able load more group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument());
+    expect(await screen.findAllByRole('row')).toHaveLength(3);
+
+    await act(async () => {
+      await user.click(await ui.showMore.find());
+    });
+
+    expect(await screen.findAllByRole('row')).toHaveLength(5);
+  });
+});
+
+describe('in manage mode', () => {
+  beforeEach(() => {
+    handler.setIsManaged(true);
+  });
+
+  it('should not be able to create a group', async () => {
+    renderGroupsApp();
+    await act(async () => expect(await ui.createGroupButton.find()).toBeDisabled());
+    expect(ui.infoManageMode.get()).toBeInTheDocument();
+  });
+
+  it('should ONLY be able to delete a local group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument());
+
+    await act(async () => {
+      await user.click(await ui.localFilter.find());
+      await user.click(await ui.localEditButton.find());
+    });
+    expect(ui.updateButton.query()).not.toBeInTheDocument();
+
+    await act(async () => {
+      await user.click(await ui.deleteButton.find());
+    });
+
+    expect(await ui.deleteDialog.find()).toBeInTheDocument();
+    await act(async () => {
+      await user.click(ui.deleteDialogButton.get());
+    });
+    expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument();
+  });
+
+  it('should not be able to delete or edit a managed group', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.managedGroupRow.find()).toBeInTheDocument());
+    expect(ui.managedEditButton.query()).not.toBeInTheDocument();
+
+    expect(ui.managedGroupEditMembersButton.query()).not.toBeInTheDocument();
+
+    await act(() => user.click(ui.managedGroupViewMembersButton.get()));
+    expect(await ui.membersViewDialog.find()).toBeInTheDocument();
+
+    expect(ui.memberAliceUser.get()).toBeInTheDocument();
+    expect(ui.memberBobUser.get()).toBeInTheDocument();
+
+    await act(() => user.type(ui.memberSearchInput.get(), 'b'));
+
+    expect(await ui.memberBobUser.find()).toBeInTheDocument();
+    expect(ui.memberAliceUser.query()).not.toBeInTheDocument();
+  });
+
+  it('should render list of all groups', async () => {
+    renderGroupsApp();
+
+    await act(async () => expect(await ui.allFilter.find()).toBeInTheDocument());
+
+    expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
+    expect(ui.managedGroupRow.get()).toBeInTheDocument();
+  });
+
+  it('should render list of managed groups', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => {
+      await user.click(await ui.managedFilter.find());
+    });
+
+    expect(ui.localGroupRow.query()).not.toBeInTheDocument();
+    expect(ui.managedGroupRow.get()).toBeInTheDocument();
+  });
+
+  it('should render list of local groups', async () => {
+    const user = userEvent.setup();
+    renderGroupsApp();
+
+    await act(async () => {
+      await user.click(await ui.localFilter.find());
+    });
+
+    expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
+    expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
+  });
+});
+
+function renderGroupsApp() {
+  return renderApp('admin/groups', <App />);
+}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/components/GroupsApp.tsx
deleted file mode 100644 (file)
index 3e72b40..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { useCallback, useEffect, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
-import { searchUsersGroups } from '../../../api/user_groups';
-import ListFooter from '../../../components/controls/ListFooter';
-import { ManagedFilter } from '../../../components/controls/ManagedFilter';
-import SearchBox from '../../../components/controls/SearchBox';
-import Suggestions from '../../../components/embed-docs-modal/Suggestions';
-import { useManageProvider } from '../../../components/hooks/useManageProvider';
-import { translate } from '../../../helpers/l10n';
-import { Group, Paging } from '../../../types/types';
-import '../groups.css';
-import Header from './Header';
-import List from './List';
-
-export default function App() {
-  const [loading, setLoading] = useState<boolean>(true);
-  const [paging, setPaging] = useState<Paging>();
-  const [search, setSearch] = useState<string>('');
-  const [groups, setGroups] = useState<Group[]>([]);
-  const [managed, setManaged] = useState<boolean | undefined>();
-  const manageProvider = useManageProvider();
-
-  const fetchGroups = useCallback(async () => {
-    setLoading(true);
-    try {
-      const { groups, paging } = await searchUsersGroups({
-        q: search,
-        managed,
-      });
-      setGroups(groups);
-      setPaging(paging);
-    } finally {
-      setLoading(false);
-    }
-  }, [search, managed]);
-
-  const fetchMoreGroups = useCallback(async () => {
-    if (!paging) {
-      return;
-    }
-    setLoading(true);
-    try {
-      const { groups: nextGroups, paging: nextPage } = await searchUsersGroups({
-        q: search,
-        managed,
-        p: paging.pageIndex + 1,
-      });
-      setPaging(nextPage);
-      setGroups([...groups, ...nextGroups]);
-    } finally {
-      setLoading(false);
-    }
-  }, [groups, search, managed, paging]);
-
-  useEffect(() => {
-    fetchGroups();
-  }, [search, managed]);
-
-  return (
-    <>
-      <Suggestions suggestions="user_groups" />
-      <Helmet defer={false} title={translate('user_groups.page')} />
-      <main className="page page-limited" id="groups-page">
-        <Header reload={fetchGroups} manageProvider={manageProvider} />
-
-        <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
-          <ManagedFilter
-            manageProvider={manageProvider}
-            loading={loading}
-            managed={managed}
-            setManaged={setManaged}
-          />
-          <SearchBox
-            id="groups-search"
-            minLength={2}
-            onChange={(q) => setSearch(q)}
-            placeholder={translate('search.search_by_name')}
-            value={search}
-          />
-        </div>
-
-        <List groups={groups} reload={fetchGroups} manageProvider={manageProvider} />
-
-        {paging !== undefined && (
-          <div id="groups-list-footer">
-            <ListFooter
-              count={groups.length}
-              loading={loading}
-              loadMore={fetchMoreGroups}
-              ready={!loading}
-              total={paging.total}
-            />
-          </div>
-        )}
-      </main>
-    </>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx
deleted file mode 100644 (file)
index d0ba4a3..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../../api/user_groups';
-import SelectList, { SelectListFilter } from '../../../../components/controls/SelectList';
-import { mockGroup } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import EditMembersModal from '../EditMembersModal';
-
-const group = mockGroup({ name: 'foo', membersCount: 1 });
-
-jest.mock('../../../../api/user_groups', () => ({
-  getUsersInGroup: jest.fn().mockResolvedValue({
-    paging: { pageIndex: 1, pageSize: 10, total: 1 },
-    users: [
-      {
-        login: 'foo',
-        name: 'bar',
-        selected: true,
-      },
-    ],
-  }),
-  addUserToGroup: jest.fn().mockResolvedValue({}),
-  removeUserFromGroup: jest.fn().mockResolvedValue({}),
-}));
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-it('should render modal properly', async () => {
-  const wrapper = shallowRender();
-  wrapper.find(SelectList).props().onSearch({
-    query: '',
-    filter: SelectListFilter.Selected,
-    page: 1,
-    pageSize: 100,
-  });
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().needToReload).toBe(false);
-
-  expect(wrapper.instance().mounted).toBe(true);
-  expect(wrapper).toMatchSnapshot();
-  expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
-  expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();
-
-  expect(getUsersInGroup).toHaveBeenCalledWith(
-    expect.objectContaining({
-      name: group.name,
-      p: 1,
-      ps: 100,
-      q: undefined,
-      selected: SelectListFilter.Selected,
-    })
-  );
-
-  wrapper.instance().componentWillUnmount();
-  expect(wrapper.instance().mounted).toBe(false);
-});
-
-it('should handle selection properly', async () => {
-  const wrapper = shallowRender();
-  wrapper.instance().handleSelect('toto');
-  await waitAndUpdate(wrapper);
-
-  expect(addUserToGroup).toHaveBeenCalledWith(
-    expect.objectContaining({
-      name: group.name,
-      login: 'toto',
-    })
-  );
-  expect(wrapper.state().needToReload).toBe(true);
-});
-
-it('should handle deselection properly', async () => {
-  const wrapper = shallowRender();
-  wrapper.instance().handleUnselect('tata');
-
-  await waitAndUpdate(wrapper);
-  expect(removeUserFromGroup).toHaveBeenCalledWith(
-    expect.objectContaining({
-      name: group.name,
-      login: 'tata',
-    })
-  );
-  expect(wrapper.state().needToReload).toBe(true);
-});
-
-function shallowRender(props: Partial<EditMembersModal['props']> = {}) {
-  return shallow<EditMembersModal>(
-    <EditMembersModal group={group} onClose={jest.fn()} {...props} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx
deleted file mode 100644 (file)
index 28580e6..0000000
+++ /dev/null
@@ -1,295 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import { act } from 'react-dom/test-utils';
-import { byRole, byText } from 'testing-library-selector';
-import GroupsServiceMock from '../../../../api/mocks/GroupsServiceMock';
-import { renderApp } from '../../../../helpers/testReactTestingUtils';
-import App from '../GroupsApp';
-
-jest.mock('../../../../api/users');
-jest.mock('../../../../api/system');
-jest.mock('../../../../api/user_groups');
-
-const handler = new GroupsServiceMock();
-
-const ui = {
-  createGroupButton: byRole('button', { name: 'groups.create_group' }),
-  infoManageMode: byText(/groups\.page\.managed_description/),
-  description: byText('user_groups.page.description'),
-  allFilter: byRole('button', { name: 'all' }),
-  managedFilter: byRole('button', { name: 'managed' }),
-  localFilter: byRole('button', { name: 'local' }),
-  searchInput: byRole('searchbox', { name: 'search.search_by_name' }),
-  updateButton: byRole('button', { name: 'update_details' }),
-  updateDialog: byRole('dialog', { name: 'groups.update_group' }),
-  updateDialogButton: byRole('button', { name: 'update_verb' }),
-  deleteButton: byRole('button', { name: 'delete' }),
-  deleteDialog: byRole('dialog', { name: 'groups.delete_group' }),
-  deleteDialogButton: byRole('button', { name: 'delete' }),
-  showMore: byRole('button', { name: 'show_more' }),
-  nameInput: byRole('textbox', { name: 'name field_required' }),
-  descriptionInput: byRole('textbox', { name: 'description' }),
-  createGroupDialogButton: byRole('button', { name: 'create' }),
-  editGroupDialogButton: byRole('button', { name: 'groups.create_group' }),
-
-  createGroupDialog: byRole('dialog', { name: 'groups.create_group' }),
-  membersViewDialog: byRole('dialog', { name: 'users.list' }),
-  membersDialog: byRole('dialog', { name: 'users.update' }),
-
-  managedGroupRow: byRole('row', { name: 'managed-group 1' }),
-  managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }),
-  managedGroupViewMembersButton: byRole('button', { name: 'groups.users.view.managed-group' }),
-
-  memberAliceUser: byText('alice'),
-  memberBobUser: byText('bob'),
-  memberSearchInput: byRole('searchbox', { name: 'search_verb' }),
-
-  managedEditButton: byRole('button', { name: 'groups.edit.managed-group' }),
-
-  localGroupRow: byRole('row', { name: 'local-group 1' }),
-  localGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.local-group' }),
-  localGroupRow2: byRole('row', { name: 'local-group 2 1 group 2 is loco!' }),
-  editedLocalGroupRow: byRole('row', { name: 'local-group 3 1 group 3 rocks!' }),
-  localEditButton: byRole('button', { name: 'groups.edit.local-group' }),
-  localGroupRowWithLocalBadge: byRole('row', {
-    name: 'local-group local 1',
-  }),
-};
-
-describe('in non managed mode', () => {
-  beforeEach(() => {
-    handler.setIsManaged(false);
-    handler.reset();
-  });
-
-  it('should render all groups', async () => {
-    renderGroupsApp();
-
-    expect(await ui.localGroupRow.find()).toBeInTheDocument();
-    expect(ui.managedGroupRow.get()).toBeInTheDocument();
-    expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument();
-  });
-
-  it('should be able to create a group', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    expect(await ui.description.find()).toBeInTheDocument();
-    await act(async () => {
-      await user.click(ui.createGroupButton.get());
-    });
-
-    expect(await ui.createGroupDialog.find()).toBeInTheDocument();
-
-    await act(async () => {
-      await user.type(ui.nameInput.get(), 'local-group 2');
-      await user.type(ui.descriptionInput.get(), 'group 2 is loco!');
-      await user.click(ui.createGroupDialogButton.get());
-    });
-
-    expect(await ui.localGroupRow2.find()).toBeInTheDocument();
-  });
-
-  it('should be able to delete a group', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    await act(async () => {
-      await user.click(await ui.localEditButton.find());
-      await user.click(await ui.deleteButton.find());
-    });
-
-    expect(await ui.deleteDialog.find()).toBeInTheDocument();
-    await act(async () => {
-      await user.click(ui.deleteDialogButton.get());
-    });
-
-    expect(await ui.managedGroupRow.find()).toBeInTheDocument();
-    expect(ui.localGroupRow.query()).not.toBeInTheDocument();
-  });
-
-  it('should be able to edit a group', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    await act(async () => {
-      await user.click(await ui.localEditButton.find());
-      await user.click(await ui.updateButton.find());
-    });
-
-    expect(ui.updateDialog.get()).toBeInTheDocument();
-
-    await act(async () => {
-      await user.clear(ui.nameInput.get());
-      await user.type(ui.nameInput.get(), 'local-group 3');
-      await user.clear(ui.descriptionInput.get());
-      await user.type(ui.descriptionInput.get(), 'group 3 rocks!');
-    });
-
-    expect(ui.updateDialog.get()).toBeInTheDocument();
-
-    await act(async () => {
-      await user.click(ui.updateDialogButton.get());
-    });
-
-    expect(await ui.managedGroupRow.find()).toBeInTheDocument();
-    expect(await ui.editedLocalGroupRow.find()).toBeInTheDocument();
-  });
-
-  it('should be able to edit the members of a group', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    expect(await ui.localGroupRow.find()).toBeInTheDocument();
-    expect(await ui.localGroupEditMembersButton.find()).toBeInTheDocument();
-
-    await act(async () => {
-      await user.click(ui.localGroupEditMembersButton.get());
-    });
-
-    expect(await ui.membersDialog.find()).toBeInTheDocument();
-  });
-
-  it('should be able search a group', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    expect(await ui.localGroupRow.find()).toBeInTheDocument();
-    expect(ui.managedGroupRow.get()).toBeInTheDocument();
-
-    await act(async () => {
-      await user.type(await ui.searchInput.find(), 'local');
-    });
-
-    expect(await ui.localGroupRow.find()).toBeInTheDocument();
-    expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
-  });
-
-  it('should be able load more group', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    expect(await ui.localGroupRow.find()).toBeInTheDocument();
-    expect(await screen.findAllByRole('row')).toHaveLength(3);
-
-    await act(async () => {
-      await user.click(await ui.showMore.find());
-    });
-
-    expect(await screen.findAllByRole('row')).toHaveLength(5);
-  });
-});
-
-describe('in manage mode', () => {
-  beforeEach(() => {
-    handler.setIsManaged(true);
-    handler.reset();
-  });
-
-  it('should not be able to create a group', async () => {
-    renderGroupsApp();
-    expect(await ui.createGroupButton.find()).toBeDisabled();
-    expect(ui.infoManageMode.get()).toBeInTheDocument();
-  });
-
-  it('should ONLY be able to delete a local group', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument();
-
-    await act(async () => {
-      await user.click(await ui.localFilter.find());
-      await user.click(await ui.localEditButton.find());
-    });
-    expect(ui.updateButton.query()).not.toBeInTheDocument();
-
-    await act(async () => {
-      await user.click(await ui.deleteButton.find());
-    });
-
-    expect(await ui.deleteDialog.find()).toBeInTheDocument();
-    await act(async () => {
-      await user.click(ui.deleteDialogButton.get());
-    });
-    expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument();
-  });
-
-  it('should not be able to delete or edit a managed group', async () => {
-    renderGroupsApp();
-
-    expect(await ui.managedGroupRow.find()).toBeInTheDocument();
-    expect(ui.managedEditButton.query()).not.toBeInTheDocument();
-
-    expect(ui.managedGroupEditMembersButton.query()).not.toBeInTheDocument();
-
-    await userEvent.click(ui.managedGroupViewMembersButton.get());
-    expect(await ui.membersViewDialog.find()).toBeInTheDocument();
-
-    expect(ui.memberAliceUser.get()).toBeInTheDocument();
-    expect(ui.memberBobUser.get()).toBeInTheDocument();
-
-    await userEvent.type(ui.memberSearchInput.get(), 'b');
-
-    expect(await ui.memberBobUser.find()).toBeInTheDocument();
-    expect(ui.memberAliceUser.query()).not.toBeInTheDocument();
-  });
-
-  it('should render list of all groups', async () => {
-    renderGroupsApp();
-
-    expect(await ui.allFilter.find()).toBeInTheDocument();
-
-    expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
-    expect(ui.managedGroupRow.get()).toBeInTheDocument();
-  });
-
-  it('should render list of managed groups', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    await act(async () => {
-      await user.click(await ui.managedFilter.find());
-    });
-
-    expect(ui.localGroupRow.query()).not.toBeInTheDocument();
-    expect(ui.managedGroupRow.get()).toBeInTheDocument();
-  });
-
-  it('should render list of local groups', async () => {
-    const user = userEvent.setup();
-    renderGroupsApp();
-
-    await act(async () => {
-      await user.click(await ui.localFilter.find());
-    });
-
-    expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
-    expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
-  });
-});
-
-function renderGroupsApp() {
-  return renderApp('admin/groups', <App />);
-}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap
deleted file mode 100644 (file)
index 35cc168..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render modal properly 1`] = `
-<Modal
-  className="group-menbers-modal"
-  contentLabel="users.update"
-  onRequestClose={[MockFunction]}
->
-  <header
-    className="modal-head"
-  >
-    <h2>
-      users.update
-    </h2>
-  </header>
-  <div
-    className="modal-body modal-container"
-  >
-    <SelectList
-      elements={
-        [
-          "foo",
-        ]
-      }
-      needToReload={false}
-      onSearch={[Function]}
-      onSelect={[Function]}
-      onUnselect={[Function]}
-      renderElement={[Function]}
-      selectedElements={
-        [
-          "foo",
-        ]
-      }
-      withPaging={true}
-    />
-  </div>
-  <footer
-    className="modal-foot"
-  >
-    <ResetButtonLink
-      onClick={[MockFunction]}
-    >
-      done
-    </ResetButtonLink>
-  </footer>
-</Modal>
-`;
-
-exports[`should render modal properly 2`] = `
-<div
-  className="select-list-list-item"
->
-  test1
-</div>
-`;
-
-exports[`should render modal properly 3`] = `
-<div
-  className="select-list-list-item"
->
-  test_foo
-</div>
-`;
index 04bebdb5d648b5d3f2fbbfe619f02a75a535ae5f..108ec3cf60dcf741c5ed88b7bcf56882d1952abd 100644 (file)
@@ -19,7 +19,7 @@
  */
 import React from 'react';
 import { Route } from 'react-router-dom';
-import App from './components/GroupsApp';
+import App from './GroupsApp';
 
 const routes = () => <Route path="groups" element={<App />} />;
 
index 1a2c1ed45ea3d565970090897e21e18289d2f208..c1bc94a971eb09f6986c48f6c439e5b6eb2ef6e3 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { act } from '@testing-library/react';
+import { act, screen, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import selectEvent from 'react-select-event';
 import { byLabelText, byRole, byText } from 'testing-library-selector';
+import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
+import UserTokensMock from '../../../api/mocks/UserTokensMock';
 import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
+import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderApp } from '../../../helpers/testReactTestingUtils';
+import { ChangePasswordResults, CurrentUser } from '../../../types/users';
 import UsersApp from '../UsersApp';
 
-jest.mock('../../../api/users');
-jest.mock('../../../api/system');
+jest.mock('../../../api/user-tokens');
+jest.mock('../../../api/components');
+jest.mock('../../../api/settings');
 
-const handler = new UsersServiceMock();
+const userHandler = new UsersServiceMock();
+const tokenHandler = new UserTokensMock();
+const componentsHandler = new ComponentsServiceMock();
+const settingsHandler = new SettingsServiceMock();
 
 const ui = {
   createUserButton: byRole('button', { name: 'users.create_user' }),
-  infoManageMode: byText(/users\.page\.managed_description/),
-  description: byText('users.page.description'),
   allFilter: byRole('button', { name: 'all' }),
+  selectedFilter: byRole('button', { name: 'selected' }),
+  unselectedFilter: byRole('button', { name: 'unselected' }),
   managedFilter: byRole('button', { name: 'managed' }),
   localFilter: byRole('button', { name: 'local' }),
-  searchInput: byRole('searchbox', { name: 'search.search_by_login_or_name' }),
-  activityFilter: byRole('combobox', { name: 'users.activity_filter.label' }),
+  showMore: byRole('button', { name: 'show_more' }),
+  aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }),
+  aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }),
+  alicedDeactivateButton: byRole('button', { name: 'users.deactivate' }),
+  bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }),
+  bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }),
+  scmAddButton: byRole('button', { name: 'add_verb' }),
+  createUserDialogButton: byRole('button', { name: 'create' }),
+  reloadButton: byRole('button', { name: 'reload' }),
+  doneButton: byRole('button', { name: 'done' }),
+  changeButton: byRole('button', { name: 'change_verb' }),
+  revokeButton: byRole('button', { name: 'users.tokens.revoke' }),
+  generateButton: byRole('button', { name: 'users.generate' }),
+  sureButton: byRole('button', { name: 'users.tokens.sure' }),
+  updateButton: byRole('button', { name: 'update_verb' }),
+  deleteSCMButton: (value?: string) =>
+    byRole('button', {
+      name: `remove_x.users.create_user.scm_account_${value ? `x.${value}` : 'new'}`,
+    }),
+
   userRows: byRole('row', {
     name: (accessibleName) => /^[A-Z]+ /.test(accessibleName),
   }),
-  showMore: byRole('button', { name: 'show_more' }),
   aliceRow: byRole('row', {
     name: (accessibleName) => accessibleName.startsWith('AM Alice Merveille alice.merveille '),
   }),
@@ -52,16 +78,11 @@ const ui = {
     name: (accessibleName) =>
       accessibleName.startsWith('AM Alice Merveille alice.merveille local '),
   }),
-  aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }),
-  aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }),
-  alicedDeactivateButton: byRole('button', { name: 'users.deactivate' }),
-  bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }),
-  bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }),
   bobRow: byRole('row', {
     name: (accessibleName) => accessibleName.startsWith('BM Bob Marley bob.marley '),
   }),
   charlieRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('CC Charlie Cox charlie.cox local '),
+    name: (accessibleName) => accessibleName.startsWith('CC Charlie Cox charlie.cox'),
   }),
   denisRow: byRole('row', {
     name: (accessibleName) => accessibleName.startsWith('DV Denis Villeneuve denis.villeneuve '),
@@ -70,23 +91,45 @@ const ui = {
     name: (accessibleName) => accessibleName.startsWith('EG Eva Green eva.green '),
   }),
   franckRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('FG Franck Grillo franck.grillo local '),
+    name: (accessibleName) => accessibleName.startsWith('FG Franck Grillo franck.grillo '),
   }),
+  jackRow: byRole('row', { name: /Jack/ }),
+
+  dialogGroups: byRole('dialog', { name: 'users.update_groups' }),
+  getGroups: () => within(ui.dialogGroups.get()).getAllByRole('checkbox'),
+  dialogTokens: byRole('dialog', { name: 'users.tokens' }),
+  dialogPasswords: byRole('dialog', { name: 'my_profile.password.title' }),
+  dialogUpdateUser: byRole('dialog', { name: 'users.update_user' }),
+  dialogCreateUser: byRole('dialog', { name: 'users.create_user' }),
+  dialogDeactivateUser: byRole('dialog', { name: 'users.deactivate_user' }),
+
+  infoManageMode: byText(/users\.page\.managed_description/),
+  description: byText('users.page.description'),
+  deleteUserAlert: byText('delete-user-warning'),
+
+  searchInput: byRole('searchbox', { name: 'search.search_by_login_or_name' }),
+  activityFilter: byRole('combobox', { name: 'users.activity_filter.label' }),
   loginInput: byRole('textbox', { name: /login/ }),
   userNameInput: byRole('textbox', { name: /name/ }),
+  emailInput: byRole('textbox', { name: /email/ }),
   passwordInput: byLabelText(/password/),
-  scmAddButton: byRole('button', { name: 'add_verb' }),
-  createUserDialogButton: byRole('button', { name: 'create' }),
   dialogSCMInputs: byRole('textbox', { name: /users.create_user.scm_account/ }),
   dialogSCMInput: (value?: string) =>
     byRole('textbox', { name: `users.create_user.scm_account_${value ? `x.${value}` : 'new'}` }),
-  deleteSCMButton: (value?: string) =>
-    byRole('button', {
-      name: `remove_x.users.create_user.scm_account_${value ? `x.${value}` : 'new'}`,
-    }),
-  jackRow: byRole('row', { name: /Jack/ }),
+  oldPassword: byLabelText('my_profile.password.old', { selector: 'input', exact: false }),
+  newPassword: byLabelText('my_profile.password.new', { selector: 'input', exact: false }),
+  confirmPassword: byLabelText('my_profile.password.confirm', { selector: 'input', exact: false }),
+  tokenNameInput: byRole('textbox', { name: 'users.tokens.name' }),
+  deleteUserCheckbox: byRole('checkbox', { name: 'users.delete_user' }),
 };
 
+beforeEach(() => {
+  tokenHandler.reset();
+  userHandler.reset();
+  componentsHandler.reset();
+  settingsHandler.reset();
+});
+
 describe('different filters combinations', () => {
   beforeAll(() => {
     jest.useFakeTimers({
@@ -102,13 +145,14 @@ describe('different filters combinations', () => {
   it('should display all users with default filters', async () => {
     renderUsersApp();
 
-    expect(await ui.userRows.findAll()).toHaveLength(6);
+    await act(async () => expect(await ui.userRows.findAll()).toHaveLength(6));
   });
 
   it('should display users filtered with text search', async () => {
+    const user = userEvent.setup();
     renderUsersApp();
 
-    await userEvent.type(await ui.searchInput.find(), 'ar');
+    await act(async () => user.type(await ui.searchInput.find(), 'ar'));
 
     expect(await ui.userRows.findAll()).toHaveLength(2);
     expect(ui.bobRow.get()).toBeInTheDocument();
@@ -116,9 +160,10 @@ describe('different filters combinations', () => {
   });
 
   it('should display local active SonarLint users', async () => {
+    const user = userEvent.setup();
     renderUsersApp();
 
-    await userEvent.click(await ui.localFilter.find());
+    await act(async () => user.click(await ui.localFilter.find()));
     await act(async () => {
       await selectEvent.select(
         ui.activityFilter.get(),
@@ -131,9 +176,10 @@ describe('different filters combinations', () => {
   });
 
   it('should display managed active SonarQube users', async () => {
+    const user = userEvent.setup();
     renderUsersApp();
 
-    await userEvent.click(await ui.managedFilter.find());
+    await act(async () => user.click(await ui.managedFilter.find()));
     await act(async () => {
       await selectEvent.select(
         ui.activityFilter.get(),
@@ -146,9 +192,10 @@ describe('different filters combinations', () => {
   });
 
   it('should display all inactive users', async () => {
+    const user = userEvent.setup();
     renderUsersApp();
 
-    await userEvent.click(await ui.allFilter.find());
+    await act(async () => user.click(await ui.allFilter.find()));
     await act(async () => {
       await selectEvent.select(ui.activityFilter.get(), 'users.activity_filter.inactive_users');
     });
@@ -161,55 +208,63 @@ describe('different filters combinations', () => {
 
 describe('in non managed mode', () => {
   beforeEach(() => {
-    handler.setIsManaged(false);
-  });
-
-  afterAll(() => {
-    handler.reset();
+    userHandler.setIsManaged(false);
   });
 
   it('should allow the creation of user', async () => {
+    const user = userEvent.setup();
     renderUsersApp();
 
-    expect(await ui.description.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.description.find()).toBeInTheDocument());
     expect(ui.createUserButton.get()).toBeEnabled();
-    await userEvent.click(ui.createUserButton.get());
+    await user.click(ui.createUserButton.get());
+
+    expect(await ui.dialogCreateUser.find()).toBeInTheDocument();
 
-    await userEvent.type(ui.loginInput.get(), 'Login');
-    await userEvent.type(ui.userNameInput.get(), 'Jack');
-    await userEvent.type(ui.passwordInput.get(), 'Password');
+    await user.type(ui.loginInput.get(), 'Login');
+    await user.type(ui.userNameInput.get(), 'Jack');
+    await user.type(ui.passwordInput.get(), 'Password');
     // Add SCM account
     expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0);
-    await userEvent.click(ui.scmAddButton.get());
+    await user.click(ui.scmAddButton.get());
     expect(ui.dialogSCMInputs.getAll()).toHaveLength(1);
-    await userEvent.type(ui.dialogSCMInput().get(), 'SCM');
+    await user.type(ui.dialogSCMInput().get(), 'SCM');
     expect(ui.dialogSCMInput('SCM').get()).toBeInTheDocument();
+    // Clear input to get an error on save
+    await user.clear(ui.dialogSCMInput('SCM').get());
+    await act(() => user.click(ui.createUserDialogButton.get()));
+    expect(ui.dialogCreateUser.get()).toBeInTheDocument();
+    expect(
+      await within(ui.dialogCreateUser.get()).findByText('Error: Empty SCM')
+    ).toBeInTheDocument();
     // Remove SCM account
-    await userEvent.click(ui.deleteSCMButton('SCM').get());
+    await user.click(ui.deleteSCMButton().get());
     expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0);
 
-    await userEvent.click(ui.createUserDialogButton.get());
+    await act(() => user.click(ui.createUserDialogButton.get()));
     expect(ui.jackRow.get()).toBeInTheDocument();
+    expect(ui.dialogCreateUser.query()).not.toBeInTheDocument();
   });
 
-  it("should be able to add/remove user's group", async () => {
+  it("should be able to see user's group", async () => {
+    const user = userEvent.setup();
     renderUsersApp();
 
-    expect(await ui.aliceUpdateGroupButton.find()).toBeInTheDocument();
+    await act(async () =>
+      expect(await within(await ui.aliceRow.find()).findByText('group1')).toBeInTheDocument()
+    );
+    expect(within(ui.aliceRow.get()).queryByText('group4')).not.toBeInTheDocument();
+    expect(within(ui.aliceRow.get()).getByText('more_x.2')).toBeInTheDocument();
+    await user.click(within(ui.aliceRow.get()).getByText('more_x.2'));
+    expect(within(ui.aliceRow.get()).queryByText('more_x.2')).not.toBeInTheDocument();
+    expect(await within(ui.aliceRow.get()).findByText('group4')).toBeInTheDocument();
     expect(ui.bobUpdateGroupButton.get()).toBeInTheDocument();
   });
 
-  it('should be able to update / change password / deactivate a user', async () => {
-    renderUsersApp();
-
-    expect(await ui.aliceUpdateButton.find()).toBeInTheDocument();
-    expect(ui.bobUpdateButton.get()).toBeInTheDocument();
-  });
-
   it('should render all users', async () => {
     renderUsersApp();
 
-    expect(await ui.aliceRow.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.aliceRow.find()).toBeInTheDocument());
     expect(ui.bobRow.get()).toBeInTheDocument();
     expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
   });
@@ -218,34 +273,187 @@ describe('in non managed mode', () => {
     const user = userEvent.setup();
     renderUsersApp();
 
-    expect(await ui.aliceRow.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.aliceRow.find()).toBeInTheDocument());
     expect(ui.bobRow.get()).toBeInTheDocument();
-    expect(ui.userRows.getAll()).toHaveLength(7);
+    expect(ui.userRows.getAll()).toHaveLength(6);
 
     await act(async () => {
       await user.click(await ui.showMore.find());
     });
 
-    expect(ui.userRows.getAll()).toHaveLength(9);
+    expect(ui.userRows.getAll()).toHaveLength(8);
+  });
+
+  it('should be able to edit the groups of a user', async () => {
+    const user = userEvent.setup();
+    renderUsersApp();
+
+    await act(async () =>
+      user.click(
+        await within(await ui.aliceRow.find()).findByRole('button', {
+          name: 'users.update_users_groups.alice.merveille',
+        })
+      )
+    );
+    expect(await ui.dialogGroups.find()).toBeInTheDocument();
+
+    expect(ui.getGroups()).toHaveLength(2);
+
+    await user.click(await ui.allFilter.find());
+    expect(ui.getGroups()).toHaveLength(3);
+
+    await user.click(ui.unselectedFilter.get());
+    expect(ui.reloadButton.query()).not.toBeInTheDocument();
+    await user.click(ui.getGroups()[0]);
+    expect(await ui.reloadButton.find()).toBeInTheDocument();
+
+    await user.click(ui.selectedFilter.get());
+    expect(ui.getGroups()).toHaveLength(3);
+    expect(ui.reloadButton.query()).not.toBeInTheDocument();
+    await user.click(ui.getGroups()[1]);
+    expect(await ui.reloadButton.find()).toBeInTheDocument();
+    await user.click(ui.reloadButton.get());
+    expect(ui.getGroups()).toHaveLength(2);
+
+    await user.type(within(ui.dialogGroups.get()).getByRole('searchbox'), '3');
+
+    expect(ui.getGroups()).toHaveLength(1);
+
+    await act(() => user.click(ui.doneButton.get()));
+    expect(ui.dialogGroups.query()).not.toBeInTheDocument();
+  });
+
+  it('should update user', async () => {
+    const user = userEvent.setup();
+    renderUsersApp();
+
+    await act(async () =>
+      user.click(
+        await within(await ui.aliceRow.find()).findByRole('button', {
+          name: 'users.manage_user.alice.merveille',
+        })
+      )
+    );
+    await user.click(
+      await within(ui.aliceRow.get()).findByRole('button', { name: 'update_details' })
+    );
+    expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
+
+    expect(ui.userNameInput.get()).toHaveValue('Alice Merveille');
+    expect(ui.emailInput.get()).toHaveValue('');
+    await user.type(ui.userNameInput.get(), '1');
+    await user.type(ui.emailInput.get(), 'test@test.com');
+    await act(() => user.click(ui.updateButton.get()));
+    expect(ui.dialogUpdateUser.query()).not.toBeInTheDocument();
+    expect(await screen.findByText('Alice Merveille1')).toBeInTheDocument();
+    expect(await screen.findByText('test@test.com')).toBeInTheDocument();
+  });
+
+  it('should deactivate user', async () => {
+    const user = userEvent.setup();
+    renderUsersApp();
+
+    await act(async () =>
+      user.click(
+        await within(await ui.aliceRow.find()).findByRole('button', {
+          name: 'users.manage_user.alice.merveille',
+        })
+      )
+    );
+    await user.click(
+      await within(ui.aliceRow.get()).findByRole('button', { name: 'users.deactivate' })
+    );
+    expect(await ui.dialogDeactivateUser.find()).toBeInTheDocument();
+    expect(ui.deleteUserAlert.query()).not.toBeInTheDocument();
+    await user.click(ui.deleteUserCheckbox.get());
+    expect(await ui.deleteUserAlert.find()).toBeInTheDocument();
+
+    await act(() =>
+      user.click(
+        within(ui.dialogDeactivateUser.get()).getByRole('button', { name: 'users.deactivate' })
+      )
+    );
+    expect(ui.aliceRow.query()).not.toBeInTheDocument();
+  });
+
+  it('should change a password', async () => {
+    const user = userEvent.setup();
+    const currentUser = mockLoggedInUser({ login: 'alice.merveille' });
+    renderUsersApp(currentUser);
+
+    await act(async () =>
+      user.click(
+        await within(await ui.aliceRow.find()).findByRole('button', {
+          name: 'users.manage_user.alice.merveille',
+        })
+      )
+    );
+    await user.click(
+      await within(ui.aliceRow.get()).findByRole('button', { name: 'my_profile.password.title' })
+    );
+    expect(await ui.dialogPasswords.find()).toBeInTheDocument();
+
+    expect(await ui.oldPassword.find()).toBeInTheDocument();
+
+    expect(ui.changeButton.get()).toBeDisabled();
+
+    await user.type(ui.oldPassword.get(), '123');
+    await user.type(ui.newPassword.get(), '1234');
+    await user.type(ui.confirmPassword.get(), '1234');
+
+    expect(ui.changeButton.get()).toBeEnabled();
+    expect(
+      screen.queryByText(`user.${ChangePasswordResults.OldPasswordIncorrect}`)
+    ).not.toBeInTheDocument();
+    await user.click(ui.changeButton.get());
+    expect(
+      await within(ui.dialogPasswords.get()).findByText(
+        `user.${ChangePasswordResults.OldPasswordIncorrect}`
+      )
+    ).toBeInTheDocument();
+
+    await user.clear(ui.oldPassword.get());
+    await user.clear(ui.newPassword.get());
+    await user.clear(ui.confirmPassword.get());
+    await user.type(ui.oldPassword.get(), 'test');
+    await user.type(ui.newPassword.get(), 'test');
+    await user.type(ui.confirmPassword.get(), 'test');
+
+    expect(
+      screen.queryByText(`user.${ChangePasswordResults.NewPasswordSameAsOld}`)
+    ).not.toBeInTheDocument();
+    await user.click(ui.changeButton.get());
+    expect(
+      await screen.findByText(`user.${ChangePasswordResults.NewPasswordSameAsOld}`)
+    ).toBeInTheDocument();
+
+    await user.clear(ui.newPassword.get());
+    await user.clear(ui.confirmPassword.get());
+    await user.type(ui.newPassword.get(), 'test2');
+    await user.type(ui.confirmPassword.get(), 'test2');
+
+    await user.click(ui.changeButton.get());
+
+    expect(ui.dialogPasswords.query()).not.toBeInTheDocument();
   });
 });
 
 describe('in manage mode', () => {
   beforeEach(() => {
-    handler.setIsManaged(true);
+    userHandler.setIsManaged(true);
   });
 
   it('should not be able to create a user"', async () => {
     renderUsersApp();
 
-    expect(await ui.infoManageMode.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.infoManageMode.find()).toBeInTheDocument());
     expect(ui.createUserButton.get()).toBeDisabled();
   });
 
   it("should not be able to add/remove a user's group", async () => {
     renderUsersApp();
 
-    expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument());
     expect(ui.aliceUpdateGroupButton.query()).not.toBeInTheDocument();
     expect(ui.bobRow.get()).toBeInTheDocument();
     expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument();
@@ -254,7 +462,7 @@ describe('in manage mode', () => {
   it('should not be able to update / change password / deactivate a managed user', async () => {
     renderUsersApp();
 
-    expect(await ui.bobRow.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.bobRow.find()).toBeInTheDocument());
     expect(ui.bobUpdateButton.query()).not.toBeInTheDocument();
   });
 
@@ -262,7 +470,7 @@ describe('in manage mode', () => {
     const user = userEvent.setup();
     renderUsersApp();
 
-    expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument());
     await user.click(ui.aliceUpdateButton.get());
     expect(await ui.alicedDeactivateButton.find()).toBeInTheDocument();
   });
@@ -270,7 +478,7 @@ describe('in manage mode', () => {
   it('should render list of all users', async () => {
     renderUsersApp();
 
-    expect(await ui.allFilter.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.allFilter.find()).toBeInTheDocument());
 
     expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
     expect(ui.bobRow.get()).toBeInTheDocument();
@@ -280,11 +488,9 @@ describe('in manage mode', () => {
     const user = userEvent.setup();
     renderUsersApp();
 
-    expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+    await act(async () => expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument());
 
-    await act(async () => {
-      await user.click(await ui.managedFilter.find());
-    });
+    await act(async () => user.click(await ui.managedFilter.find()));
 
     expect(await ui.bobRow.find()).toBeInTheDocument();
     expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
@@ -298,11 +504,58 @@ describe('in manage mode', () => {
       await user.click(await ui.localFilter.find());
     });
 
-    expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+    expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
     expect(ui.bobRow.query()).not.toBeInTheDocument();
   });
+
+  it('should be able to change tokens of a user', async () => {
+    const user = userEvent.setup();
+    renderUsersApp();
+
+    await act(async () =>
+      user.click(
+        await within(await ui.aliceRow.find()).findByRole('button', {
+          name: 'users.update_tokens_for_x.Alice Merveille',
+        })
+      )
+    );
+    expect(await ui.dialogTokens.find()).toBeInTheDocument();
+
+    const getTokensList = () => within(ui.dialogTokens.get()).getAllByRole('row');
+
+    expect(getTokensList()).toHaveLength(3);
+
+    await user.type(ui.tokenNameInput.get(), 'test');
+    await user.click(ui.generateButton.get());
+
+    // Not deleted because there is already token with name test
+    expect(screen.queryByText('users.tokens.new_token_created.test')).not.toBeInTheDocument();
+    expect(getTokensList()).toHaveLength(3);
+
+    expect(ui.sureButton.query()).not.toBeInTheDocument();
+    await user.click(ui.revokeButton.getAll()[1]);
+    expect(await ui.sureButton.find()).toBeInTheDocument();
+    await act(() => user.click(ui.sureButton.get()));
+
+    expect(getTokensList()).toHaveLength(2);
+
+    await act(() => user.click(ui.generateButton.get()));
+    expect(getTokensList()).toHaveLength(3);
+    expect(await screen.findByText('users.tokens.new_token_created.test')).toBeInTheDocument();
+
+    await user.click(ui.doneButton.get());
+    expect(ui.dialogTokens.query()).not.toBeInTheDocument();
+  });
+});
+
+it('should render external identity Providers', async () => {
+  renderUsersApp();
+
+  await act(async () => expect(await ui.charlieRow.find()).toHaveTextContent(/ExternalTest/));
+  expect(await ui.denisRow.find()).toHaveTextContent(/test2: UnknownExternalProvider/);
 });
 
-function renderUsersApp() {
-  return renderApp('admin/users', <UsersApp />);
+function renderUsersApp(currentUser?: CurrentUser) {
+  // eslint-disable-next-line testing-library/no-unnecessary-act
+  return renderApp('admin/users', <UsersApp />, { currentUser: mockCurrentUser(currentUser) });
 }
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/DeactivateForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/DeactivateForm-test.tsx
deleted file mode 100644 (file)
index 0a9f8e1..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { screen } from '@testing-library/react';
-import * as React from 'react';
-import { deactivateUser } from '../../../../api/users';
-import { mockUser } from '../../../../helpers/testMocks';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { UserActive } from '../../../../types/users';
-import DeactivateForm from '../DeactivateForm';
-
-jest.mock('../../../../api/users', () => ({
-  deactivateUser: jest.fn().mockResolvedValue({}),
-}));
-
-it('should deactivate user with anonymize set to true', () => {
-  const user = mockUser() as UserActive;
-  renderDeactivateForm(user);
-
-  screen.getByRole('checkbox').click();
-  expect(screen.getByRole('alert')).toBeInTheDocument();
-
-  screen.getByRole('button', { name: 'users.deactivate' }).click();
-  expect(deactivateUser).toHaveBeenCalledWith({ login: user.login, anonymize: true });
-});
-
-function renderDeactivateForm(user: UserActive) {
-  return renderComponent(
-    <DeactivateForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={user} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx
deleted file mode 100644 (file)
index d793476..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { getUserGroups } from '../../../../api/users';
-import { addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups';
-import SelectList, { SelectListFilter } from '../../../../components/controls/SelectList';
-import { mockUser } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import GroupsForm from '../GroupsForm';
-
-const user = mockUser();
-
-jest.mock('../../../../api/users', () => ({
-  getUserGroups: jest.fn().mockResolvedValue({
-    paging: { pageIndex: 1, pageSize: 10, total: 1 },
-    groups: [
-      {
-        id: 1001,
-        name: 'test1',
-        description: 'test1',
-        selected: true,
-      },
-      {
-        id: 1002,
-        name: 'test2',
-        description: 'test2',
-        selected: true,
-      },
-      {
-        id: 1003,
-        name: 'test3',
-        description: 'test3',
-        selected: false,
-      },
-    ],
-  }),
-}));
-
-jest.mock('../../../../api/user_groups', () => ({
-  addUserToGroup: jest.fn().mockResolvedValue({}),
-  removeUserFromGroup: jest.fn().mockResolvedValue({}),
-}));
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-it('should render correctly', async () => {
-  const wrapper = shallowRender();
-  wrapper.find(SelectList).props().onSearch({
-    query: '',
-    filter: SelectListFilter.Selected,
-    page: 1,
-    pageSize: 100,
-  });
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.instance().mounted).toBe(true);
-  expect(wrapper).toMatchSnapshot();
-  expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
-  expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();
-
-  expect(getUserGroups).toHaveBeenCalledWith(
-    expect.objectContaining({
-      login: user.login,
-      p: 1,
-      ps: 100,
-      q: undefined,
-      selected: SelectListFilter.Selected,
-    })
-  );
-  expect(wrapper.state().needToReload).toBe(false);
-
-  wrapper.instance().componentWillUnmount();
-  expect(wrapper.instance().mounted).toBe(false);
-});
-
-it('should handle selection properly', async () => {
-  const wrapper = shallowRender();
-  wrapper.instance().handleSelect('toto');
-  await waitAndUpdate(wrapper);
-
-  expect(addUserToGroup).toHaveBeenCalledWith(
-    expect.objectContaining({
-      login: user.login,
-      name: 'toto',
-    })
-  );
-  expect(wrapper.state().needToReload).toBe(true);
-});
-
-it('should handle deselection properly', async () => {
-  const wrapper = shallowRender();
-  wrapper.instance().handleUnselect('tata');
-  await waitAndUpdate(wrapper);
-
-  expect(removeUserFromGroup).toHaveBeenCalledWith(
-    expect.objectContaining({
-      login: user.login,
-      name: 'tata',
-    })
-  );
-  expect(wrapper.state().needToReload).toBe(true);
-});
-
-function shallowRender(props: Partial<GroupsForm['props']> = {}) {
-  return shallow<GroupsForm>(
-    <GroupsForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={user} {...props} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/PasswordForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/PasswordForm-test.tsx
deleted file mode 100644 (file)
index 6b81c91..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { changePassword } from '../../../../api/users';
-import { mockUser } from '../../../../helpers/testMocks';
-import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils';
-import { ChangePasswordResults } from '../../../../types/users';
-import PasswordForm from '../PasswordForm';
-
-const password = 'new password asdf';
-
-jest.mock('../../../../api/users', () => ({
-  changePassword: jest.fn(() => Promise.resolve()),
-}));
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should handle password change', async () => {
-  const onClose = jest.fn();
-  const wrapper = shallowRender({ onClose });
-
-  wrapper.setState({ newPassword: password, confirmPassword: password });
-  wrapper.instance().handleChangePassword(mockEvent({ preventDefault: jest.fn() }));
-
-  await waitAndUpdate(wrapper);
-
-  expect(onClose).toHaveBeenCalled();
-});
-
-it('should handle password change error when new password is same as old', async () => {
-  const wrapper = shallowRender();
-
-  jest.mocked(changePassword).mockRejectedValue(ChangePasswordResults.NewPasswordSameAsOld);
-  wrapper.setState({ newPassword: password, confirmPassword: password });
-  wrapper.instance().mounted = true;
-  wrapper.instance().handleChangePassword(mockEvent({ preventDefault: jest.fn() }));
-
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.state().errorTranslationKey).toBe('user.new_password_same_as_old');
-});
-
-it('should handle password change error when old password is incorrect', async () => {
-  const wrapper = shallowRender();
-
-  jest.mocked(changePassword).mockRejectedValue(ChangePasswordResults.OldPasswordIncorrect);
-
-  wrapper.setState({ newPassword: password, confirmPassword: password });
-  wrapper.instance().mounted = true;
-  wrapper.instance().handleChangePassword(mockEvent({ preventDefault: jest.fn() }));
-
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.state().errorTranslationKey).toBe('user.old_password_incorrect');
-});
-
-it('should handle form changes', () => {
-  const wrapper = shallowRender();
-
-  wrapper.instance().handleConfirmPasswordChange(mockEvent({ currentTarget: { value: 'pwd' } }));
-  expect(wrapper.state().confirmPassword).toBe('pwd');
-
-  wrapper.instance().handleNewPasswordChange(mockEvent({ currentTarget: { value: 'pwd' } }));
-  expect(wrapper.state().newPassword).toBe('pwd');
-
-  wrapper.instance().handleOldPasswordChange(mockEvent({ currentTarget: { value: 'pwd' } }));
-  expect(wrapper.state().oldPassword).toBe('pwd');
-});
-
-function shallowRender(props: Partial<PasswordForm['props']> = {}) {
-  return shallow<PasswordForm>(
-    <PasswordForm isCurrentUser={true} onClose={jest.fn()} user={mockUser()} {...props} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx
deleted file mode 100644 (file)
index 9f50f4e..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { revokeToken } from '../../../../api/user-tokens';
-import { mockUserToken } from '../../../../helpers/mocks/token';
-import { click, waitAndUpdate } from '../../../../helpers/testUtils';
-import TokensFormItem from '../TokensFormItem';
-
-jest.mock('../../../../components/intl/DateFormatter');
-jest.mock('../../../../components/intl/DateFromNow');
-jest.mock('../../../../components/intl/DateTimeFormatter');
-
-jest.mock('../../../../api/user-tokens', () => ({
-  revokeToken: jest.fn().mockResolvedValue(undefined),
-}));
-
-const userToken = mockUserToken({
-  name: 'foo',
-  createdAt: '2019-01-15T15:06:33+0100',
-  lastConnectionDate: '2019-01-18T15:06:33+0100',
-});
-
-beforeEach(() => {
-  (revokeToken as jest.Mock).mockClear();
-});
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-  expect(shallowRender({ deleteConfirmation: 'modal' })).toMatchSnapshot();
-});
-
-it('should revoke the token using inline confirmation', async () => {
-  const onRevokeToken = jest.fn();
-  const wrapper = shallowRender({ deleteConfirmation: 'inline', onRevokeToken });
-  expect(wrapper.find('Button')).toMatchSnapshot();
-  click(wrapper.find('Button'));
-  expect(wrapper.find('Button')).toMatchSnapshot();
-  click(wrapper.find('Button'));
-  expect(wrapper.find('DeferredSpinner').prop('loading')).toBe(true);
-  await waitAndUpdate(wrapper);
-  expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' });
-  expect(onRevokeToken).toHaveBeenCalledWith(userToken);
-});
-
-it('should revoke the token using modal confirmation', async () => {
-  const onRevokeToken = jest.fn();
-  const wrapper = shallowRender({ deleteConfirmation: 'modal', onRevokeToken });
-  wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
-  expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' });
-  await waitAndUpdate(wrapper);
-  expect(onRevokeToken).toHaveBeenCalledWith(userToken);
-});
-
-function shallowRender(props: Partial<TokensFormItem['props']> = {}) {
-  return shallow(
-    <TokensFormItem
-      deleteConfirmation="inline"
-      login="luke"
-      onRevokeToken={jest.fn()}
-      token={userToken}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormModal-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormModal-test.tsx
deleted file mode 100644 (file)
index 14b3ef3..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { mockUser } from '../../../../helpers/testMocks';
-import TokensFormModal from '../TokensFormModal';
-
-it('should render correctly', () => {
-  expect(
-    shallow(<TokensFormModal onClose={jest.fn()} updateTokensCount={jest.fn()} user={mockUser()} />)
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx
deleted file mode 100644 (file)
index f040b60..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 } from '../../../../helpers/testUtils';
-import UserActions from '../UserActions';
-
-const user = {
-  login: 'obi',
-  name: 'One',
-  active: true,
-  scmAccounts: [],
-  local: false,
-  managed: false,
-};
-
-it('should render correctly', () => {
-  expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should open the update form', () => {
-  const wrapper = getWrapper();
-  click(wrapper.find('.js-user-update'));
-  expect(wrapper.first().find('UserForm').exists()).toBe(true);
-});
-
-it('should open the password form', () => {
-  const wrapper = getWrapper({ user: { ...user, local: true } });
-  click(wrapper.find('.js-user-change-password'));
-  expect(wrapper.first().find('PasswordForm').exists()).toBe(true);
-});
-
-it('should open the deactivate form', () => {
-  const wrapper = getWrapper();
-  click(wrapper.find('.js-user-deactivate'));
-  expect(wrapper.first().find('DeactivateForm').exists()).toBe(true);
-});
-
-function getWrapper(props = {}) {
-  return shallow(
-    <UserActions
-      isCurrentUser={false}
-      onUpdateUsers={jest.fn()}
-      user={user}
-      manageProvider={undefined}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx
deleted file mode 100644 (file)
index a2cfd5e..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { createUser, updateUser } from '../../../../api/users';
-import { Alert } from '../../../../components/ui/Alert';
-import { mockUser } from '../../../../helpers/testMocks';
-import { submit, waitAndUpdate } from '../../../../helpers/testUtils';
-import UserForm from '../UserForm';
-
-jest.mock('../../../../api/users', () => ({
-  createUser: jest.fn().mockResolvedValue({}),
-  updateUser: jest.fn().mockResolvedValue({}),
-}));
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-it('should render correctly', () => {
-  expect(shallowRender().dive()).toMatchSnapshot();
-  expect(shallowRender({ user: undefined }).dive()).toMatchSnapshot();
-});
-
-it('should correctly show errors', async () => {
-  const response = new Response(null, { status: 400 });
-  response.json = jest.fn().mockRejectedValue(undefined);
-
-  (updateUser as jest.Mock).mockRejectedValue(response);
-
-  const wrapper = shallowRender();
-  submit(wrapper.dive().find('form'));
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.dive().find(Alert).children().text()).toMatch('default_error_message');
-});
-
-it('should correctly disable name and email fields for non-local users', () => {
-  const wrapper = shallowRender({ user: mockUser({ local: false }) }).dive();
-  expect(wrapper.find('#create-user-name').prop('disabled')).toBe(true);
-  expect(wrapper.find('#create-user-email').prop('disabled')).toBe(true);
-  expect(wrapper.find('Alert').exists()).toBe(true);
-  expect(wrapper.find(Alert).children().text()).toMatch('users.cannot_update_delegated_user');
-});
-
-it('should correctly create a new user', () => {
-  const email = 'foo@bar.ch';
-  const login = 'foo';
-  const name = 'Foo';
-  const password = 'bar';
-  const scmAccounts = ['gh', 'gh', 'bitbucket'];
-  const wrapper = shallowRender({ user: undefined });
-
-  wrapper.setState({ email, login, name, password, scmAccounts });
-
-  submit(wrapper.dive().find('form'));
-
-  expect(createUser).toHaveBeenCalledWith({
-    email,
-    login,
-    name,
-    password,
-    scmAccount: ['gh', 'gh', 'bitbucket'],
-  });
-});
-
-it('should correctly update a local user', () => {
-  const email = 'foo@bar.ch';
-  const login = 'foo';
-  const name = 'Foo';
-  const scmAccounts = ['gh', 'gh', 'bitbucket'];
-  const wrapper = shallowRender({ user: mockUser({ email, login, name, scmAccounts }) }).dive();
-
-  submit(wrapper.find('form'));
-
-  expect(updateUser).toHaveBeenCalledWith({
-    email,
-    login,
-    name,
-    scmAccount: ['gh', 'gh', 'bitbucket'],
-  });
-});
-
-it('should correctly update a non-local user', () => {
-  const email = 'foo@bar.ch';
-  const login = 'foo';
-  const name = 'Foo';
-  const scmAccounts = ['gh', 'bitbucket'];
-  const wrapper = shallowRender({
-    user: mockUser({ email, local: false, login, name, scmAccounts }),
-  }).dive();
-
-  submit(wrapper.find('form'));
-
-  expect(updateUser).toHaveBeenCalledWith(
-    expect.not.objectContaining({
-      email,
-      name,
-    })
-  );
-});
-
-function shallowRender(props: Partial<UserForm['props']> = {}) {
-  return shallow(
-    <UserForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={mockUser()} {...props} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx
deleted file mode 100644 (file)
index ae11594..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 } from '../../../../helpers/testUtils';
-import UserGroups from '../UserGroups';
-
-const user = {
-  login: 'obi',
-  name: 'One',
-  active: true,
-  scmAccounts: [],
-  local: false,
-  managed: false,
-};
-
-const groups = ['foo', 'bar', 'baz', 'plop'];
-
-it('should render correctly', () => {
-  expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should show all groups', () => {
-  const wrapper = getWrapper();
-  expect(wrapper.find('li')).toHaveLength(3);
-  click(wrapper.find('.js-user-more-groups'));
-  expect(wrapper.find('li')).toHaveLength(5);
-});
-
-it('should open the groups form', () => {
-  const wrapper = getWrapper();
-  click(wrapper.find('.js-user-groups'));
-  expect(wrapper.find('GroupsForm').exists()).toBe(true);
-});
-
-function getWrapper(props = {}) {
-  return shallow(
-    <UserGroups
-      groups={groups}
-      onUpdateUsers={jest.fn()}
-      user={user}
-      manageProvider={undefined}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx
deleted file mode 100644 (file)
index 76b5871..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 } from '../../../../helpers/testUtils';
-import { User } from '../../../../types/users';
-import UserListItem, { UserListItemProps } from '../UserListItem';
-
-jest.mock('../../../../components/intl/DateFromNow');
-jest.mock('../../../../components/intl/DateTimeFormatter');
-
-const user: User = {
-  active: true,
-  lastConnectionDate: '2019-01-18T15:06:33+0100',
-  sonarLintLastConnectionDate: '2019-01-16T15:06:33+0100',
-  local: false,
-  login: 'obi',
-  name: 'One',
-  scmAccounts: [],
-  managed: false,
-};
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should render correctly without last connection date', () => {
-  expect(shallowRender({})).toMatchSnapshot();
-});
-
-it('should open the correct forms', () => {
-  const wrapper = shallowRender();
-  click(wrapper.find('.js-user-tokens'));
-  expect(wrapper.find('TokensFormModal').exists()).toBe(true);
-});
-
-function shallowRender(props: Partial<UserListItemProps> = {}) {
-  return shallow(
-    <UserListItem
-      isCurrentUser={false}
-      onUpdateUsers={jest.fn()}
-      updateTokensCount={jest.fn()}
-      user={user}
-      manageProvider={undefined}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx
deleted file mode 100644 (file)
index a362fd7..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 UserListItemIdentity, { ExternalProvider, Props } from '../UserListItemIdentity';
-
-describe('#UserListItemIdentity', () => {
-  it('should render correctly', () => {
-    expect(shallowRender()).toMatchSnapshot();
-  });
-
-  function shallowRender(props: Partial<Props> = {}) {
-    return shallow(
-      <UserListItemIdentity
-        identityProvider={{
-          backgroundColor: 'blue',
-          iconPath: 'icon/path',
-          key: 'foo',
-          name: 'Foo Provider',
-        }}
-        user={{
-          active: true,
-          email: 'obi.one@empire',
-          externalProvider: 'foo',
-          lastConnectionDate: '2019-01-18T15:06:33+0100',
-          local: false,
-          login: 'obi',
-          name: 'One',
-          scmAccounts: [],
-          managed: false,
-        }}
-        {...props}
-      />
-    );
-  }
-});
-
-describe('#ExternalProvider', () => {
-  it('should render correctly', () => {
-    expect(shallowRender()).toMatchSnapshot();
-  });
-
-  it('should render the user external provider and identity', () => {
-    expect(shallowRender({ identityProvider: undefined })).toMatchSnapshot();
-  });
-
-  function shallowRender(props: Partial<Props> = {}) {
-    return shallow(
-      <ExternalProvider
-        identityProvider={{
-          backgroundColor: 'blue',
-          iconPath: 'icon/path',
-          key: 'foo',
-          name: 'Foo Provider',
-        }}
-        user={{
-          active: true,
-          email: 'obi.one@empire',
-          externalProvider: 'foo',
-          lastConnectionDate: '2019-01-18T15:06:33+0100',
-          local: false,
-          login: 'obi',
-          name: 'One',
-          scmAccounts: [],
-          managed: false,
-        }}
-        {...props}
-      />
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap
deleted file mode 100644 (file)
index cbdda1a..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Modal
-  contentLabel="users.update_groups"
-  onRequestClose={[Function]}
->
-  <div
-    className="modal-head"
-  >
-    <h2>
-      users.update_groups
-    </h2>
-  </div>
-  <div
-    className="modal-body modal-container"
-  >
-    <SelectList
-      elements={
-        [
-          "test1",
-          "test2",
-          "test3",
-        ]
-      }
-      elementsTotalCount={1}
-      needToReload={false}
-      onSearch={[Function]}
-      onSelect={[Function]}
-      onUnselect={[Function]}
-      renderElement={[Function]}
-      selectedElements={
-        [
-          "test1",
-          "test2",
-        ]
-      }
-      withPaging={true}
-    />
-  </div>
-  <footer
-    className="modal-foot"
-  >
-    <ResetButtonLink
-      onClick={[Function]}
-    >
-      done
-    </ResetButtonLink>
-  </footer>
-</Modal>
-`;
-
-exports[`should render correctly 2`] = `
-<div
-  className="select-list-list-item"
->
-  <React.Fragment>
-    test1
-    <br />
-    <span
-      className="note"
-    >
-      test1
-    </span>
-  </React.Fragment>
-</div>
-`;
-
-exports[`should render correctly 3`] = `
-<div
-  className="select-list-list-item"
->
-  test_foo
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/PasswordForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/PasswordForm-test.tsx.snap
deleted file mode 100644 (file)
index 680f7a1..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Modal
-  contentLabel="my_profile.password.title"
-  onRequestClose={[MockFunction]}
-  size="small"
->
-  <form
-    autoComplete="off"
-    id="user-password-form"
-    onSubmit={[Function]}
-  >
-    <header
-      className="modal-head"
-    >
-      <h2>
-        my_profile.password.title
-      </h2>
-    </header>
-    <div
-      className="modal-body"
-    >
-      <MandatoryFieldsExplanation
-        className="modal-field"
-      />
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="old-user-password"
-        >
-          my_profile.password.old
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          className="hidden"
-          name="old-password-fake"
-          type="password"
-        />
-        <input
-          id="old-user-password"
-          name="old-password"
-          onChange={[Function]}
-          required={true}
-          type="password"
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="user-password"
-        >
-          my_profile.password.new
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          className="hidden"
-          name="password-fake"
-          type="password"
-        />
-        <input
-          id="user-password"
-          name="password"
-          onChange={[Function]}
-          required={true}
-          type="password"
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="confirm-user-password"
-        >
-          my_profile.password.confirm
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          className="hidden"
-          name="confirm-password-fake"
-          type="password"
-        />
-        <input
-          id="confirm-user-password"
-          name="confirm-password"
-          onChange={[Function]}
-          required={true}
-          type="password"
-          value=""
-        />
-      </div>
-    </div>
-    <footer
-      className="modal-foot"
-    >
-      <SubmitButton
-        disabled={true}
-      >
-        change_verb
-      </SubmitButton>
-      <ResetButtonLink
-        onClick={[MockFunction]}
-      >
-        cancel
-      </ResetButtonLink>
-    </footer>
-  </form>
-</Modal>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap
deleted file mode 100644 (file)
index 9f54f8a..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tr>
-  <td
-    className="hide-overflow nowrap"
-    title="foo"
-  >
-    foo
-  </td>
-  <td
-    className="hide-overflow thin"
-    title="users.tokens.USER_TOKEN"
-  >
-    users.tokens.USER_TOKEN.short
-  </td>
-  <td
-    className="hide-overflow"
-  />
-  <td
-    className="thin nowrap"
-  >
-    <DateFromNow
-      date="2019-01-18T15:06:33+0100"
-      hourPrecision={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-right"
-  >
-    <DateFormatter
-      date="2019-01-15T15:06:33+0100"
-      long={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-right"
-  >
-    â€“
-  </td>
-  <td
-    className="thin nowrap text-right"
-  >
-    <Button
-      className="button-red input-small"
-      disabled={false}
-      onClick={[Function]}
-    >
-      <DeferredSpinner
-        className="little-spacer-right"
-        loading={false}
-      />
-      users.tokens.revoke
-    </Button>
-  </td>
-</tr>
-`;
-
-exports[`should render correctly 2`] = `
-<tr>
-  <td
-    className="hide-overflow nowrap"
-    title="foo"
-  >
-    foo
-  </td>
-  <td
-    className="hide-overflow thin"
-    title="users.tokens.USER_TOKEN"
-  >
-    users.tokens.USER_TOKEN.short
-  </td>
-  <td
-    className="hide-overflow"
-  />
-  <td
-    className="thin nowrap"
-  >
-    <DateFromNow
-      date="2019-01-18T15:06:33+0100"
-      hourPrecision={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-right"
-  >
-    <DateFormatter
-      date="2019-01-15T15:06:33+0100"
-      long={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-right"
-  >
-    â€“
-  </td>
-  <td
-    className="thin nowrap text-right"
-  >
-    <ConfirmButton
-      confirmButtonText="users.tokens.revoke_token"
-      isDestructive={true}
-      modalBody={
-        <FormattedMessage
-          defaultMessage="users.tokens.sure_X"
-          id="users.tokens.sure_X"
-          values={
-            {
-              "token": <strong>
-                foo
-              </strong>,
-            }
-          }
-        />
-      }
-      modalHeader="users.tokens.revoke_token"
-      onConfirm={[Function]}
-    >
-      <Component />
-    </ConfirmButton>
-  </td>
-</tr>
-`;
-
-exports[`should revoke the token using inline confirmation 1`] = `
-<Button
-  className="button-red input-small"
-  disabled={false}
-  onClick={[Function]}
->
-  <DeferredSpinner
-    className="little-spacer-right"
-    loading={false}
-  />
-  users.tokens.revoke
-</Button>
-`;
-
-exports[`should revoke the token using inline confirmation 2`] = `
-<Button
-  className="button-red input-small"
-  disabled={false}
-  onClick={[Function]}
->
-  <DeferredSpinner
-    className="little-spacer-right"
-    loading={false}
-  />
-  users.tokens.sure
-</Button>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap
deleted file mode 100644 (file)
index 153c5c0..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Modal
-  contentLabel="users.tokens"
-  onRequestClose={[MockFunction]}
-  size="large"
->
-  <header
-    className="modal-head"
-  >
-    <h2>
-      <FormattedMessage
-        defaultMessage="users.user_X_tokens"
-        id="users.user_X_tokens"
-        values={
-          {
-            "user": <em>
-              John Doe
-            </em>,
-          }
-        }
-      />
-    </h2>
-  </header>
-  <div
-    className="modal-body modal-container"
-  >
-    <withCurrentUserContext(TokensForm)
-      deleteConfirmation="inline"
-      displayTokenTypeInput={false}
-      login="john.doe"
-      updateTokensCount={[MockFunction]}
-    />
-  </div>
-  <footer
-    className="modal-foot"
-  >
-    <ResetButtonLink
-      onClick={[MockFunction]}
-    >
-      done
-    </ResetButtonLink>
-  </footer>
-</Modal>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap
deleted file mode 100644 (file)
index b00cbce..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Fragment>
-  <ActionsDropdown
-    label="users.manage_user.obi"
-  >
-    <ActionsDropdownItem
-      className="js-user-update"
-      onClick={[Function]}
-    >
-      update_details
-    </ActionsDropdownItem>
-    <ActionsDropdownDivider />
-    <ActionsDropdownItem
-      className="js-user-deactivate"
-      destructive={true}
-      onClick={[Function]}
-    >
-      users.deactivate
-    </ActionsDropdownItem>
-  </ActionsDropdown>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap
deleted file mode 100644 (file)
index 8c0d43a..0000000
+++ /dev/null
@@ -1,261 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Modal
-  contentLabel="users.update_user"
-  onRequestClose={[MockFunction]}
-  size="small"
->
-  <form
-    autoComplete="off"
-    id="user-form"
-    onSubmit={[Function]}
-  >
-    <header
-      className="modal-head"
-    >
-      <h2>
-        users.update_user
-      </h2>
-    </header>
-    <div
-      className="modal-body modal-container"
-    >
-      <MandatoryFieldsExplanation
-        className="modal-field"
-      />
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="create-user-name"
-        >
-          name
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          autoComplete="off"
-          autoFocus={true}
-          disabled={false}
-          id="create-user-name"
-          maxLength={200}
-          name="name"
-          onChange={[Function]}
-          required={true}
-          type="text"
-          value="John Doe"
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="create-user-email"
-        >
-          users.email
-        </label>
-        <input
-          autoComplete="off"
-          disabled={false}
-          id="create-user-email"
-          maxLength={100}
-          name="email"
-          onChange={[Function]}
-          type="email"
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <fieldset>
-          <legend>
-            my_profile.scm_accounts
-          </legend>
-          <div
-            className="spacer-bottom"
-          >
-            <Button
-              className="js-scm-account-add"
-              onClick={[Function]}
-            >
-              add_verb
-            </Button>
-          </div>
-        </fieldset>
-        <p
-          className="note"
-        >
-          user.login_or_email_used_as_scm_account
-        </p>
-      </div>
-    </div>
-    <footer
-      className="modal-foot"
-    >
-      <SubmitButton
-        disabled={false}
-      >
-        update_verb
-      </SubmitButton>
-      <ResetButtonLink
-        onClick={[Function]}
-      >
-        cancel
-      </ResetButtonLink>
-    </footer>
-  </form>
-</Modal>
-`;
-
-exports[`should render correctly 2`] = `
-<Modal
-  contentLabel="users.create_user"
-  onRequestClose={[MockFunction]}
-  size="small"
->
-  <form
-    autoComplete="off"
-    id="user-form"
-    onSubmit={[Function]}
-  >
-    <header
-      className="modal-head"
-    >
-      <h2>
-        users.create_user
-      </h2>
-    </header>
-    <div
-      className="modal-body modal-container"
-    >
-      <MandatoryFieldsExplanation
-        className="modal-field"
-      />
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="create-user-login"
-        >
-          login
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          autoComplete="off"
-          autoFocus={true}
-          id="create-user-login"
-          maxLength={255}
-          minLength={3}
-          name="login"
-          onChange={[Function]}
-          required={true}
-          type="text"
-          value=""
-        />
-        <p
-          className="note"
-        >
-          users.minimum_x_characters.3
-        </p>
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="create-user-name"
-        >
-          name
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          autoComplete="off"
-          autoFocus={false}
-          id="create-user-name"
-          maxLength={200}
-          name="name"
-          onChange={[Function]}
-          required={true}
-          type="text"
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="create-user-email"
-        >
-          users.email
-        </label>
-        <input
-          autoComplete="off"
-          id="create-user-email"
-          maxLength={100}
-          name="email"
-          onChange={[Function]}
-          type="email"
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="create-user-password"
-        >
-          password
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          autoComplete="off"
-          id="create-user-password"
-          name="password"
-          onChange={[Function]}
-          required={true}
-          type="password"
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <fieldset>
-          <legend>
-            my_profile.scm_accounts
-          </legend>
-          <div
-            className="spacer-bottom"
-          >
-            <Button
-              className="js-scm-account-add"
-              onClick={[Function]}
-            >
-              add_verb
-            </Button>
-          </div>
-        </fieldset>
-        <p
-          className="note"
-        >
-          user.login_or_email_used_as_scm_account
-        </p>
-      </div>
-    </div>
-    <footer
-      className="modal-foot"
-    >
-      <SubmitButton
-        disabled={false}
-      >
-        create
-      </SubmitButton>
-      <ResetButtonLink
-        onClick={[Function]}
-      >
-        cancel
-      </ResetButtonLink>
-    </footer>
-  </form>
-</Modal>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserGroups-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserGroups-test.tsx.snap
deleted file mode 100644 (file)
index 44c695e..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ul>
-  <li
-    className="little-spacer-bottom"
-    key="foo"
-  >
-    foo
-  </li>
-  <li
-    className="little-spacer-bottom"
-    key="bar"
-  >
-    bar
-  </li>
-  <li
-    className="little-spacer-bottom"
-  >
-    <ButtonLink
-      className="js-user-more-groups spacer-right"
-      onClick={[Function]}
-    >
-      more_x.2
-    </ButtonLink>
-    <ButtonIcon
-      aria-label="users.update_users_groups.obi"
-      className="js-user-groups button-small"
-      onClick={[Function]}
-      tooltip="users.update_groups"
-    >
-      <BulletListIcon />
-    </ButtonIcon>
-  </li>
-</ul>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap
deleted file mode 100644 (file)
index 3b73053..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tr>
-  <td
-    className="thin text-middle"
-  >
-    <div
-      className="sw-flex sw-items-center"
-    >
-      <withAppStateContext(Avatar)
-        className="sw-shrink-0 sw-mr-4"
-        name="One"
-        size={36}
-      />
-      <UserListItemIdentity
-        user={
-          {
-            "active": true,
-            "lastConnectionDate": "2019-01-18T15:06:33+0100",
-            "local": false,
-            "login": "obi",
-            "managed": false,
-            "name": "One",
-            "scmAccounts": [],
-            "sonarLintLastConnectionDate": "2019-01-16T15:06:33+0100",
-          }
-        }
-      />
-    </div>
-  </td>
-  <td
-    className="thin text-middle"
-  >
-    <UserScmAccounts
-      scmAccounts={[]}
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <DateFromNow
-      date="2019-01-18T15:06:33+0100"
-      hourPrecision={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <DateFromNow
-      date="2019-01-16T15:06:33+0100"
-      hourPrecision={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <UserGroups
-      groups={[]}
-      onUpdateUsers={[MockFunction]}
-      user={
-        {
-          "active": true,
-          "lastConnectionDate": "2019-01-18T15:06:33+0100",
-          "local": false,
-          "login": "obi",
-          "managed": false,
-          "name": "One",
-          "scmAccounts": [],
-          "sonarLintLastConnectionDate": "2019-01-16T15:06:33+0100",
-        }
-      }
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <ButtonIcon
-      aria-label="users.update_tokens_for_x.One"
-      className="js-user-tokens spacer-left button-small"
-      onClick={[Function]}
-      tooltip="users.update_tokens"
-    >
-      <BulletListIcon />
-    </ButtonIcon>
-  </td>
-  <td
-    className="thin nowrap text-right text-middle"
-  >
-    <UserActions
-      isCurrentUser={false}
-      onUpdateUsers={[MockFunction]}
-      user={
-        {
-          "active": true,
-          "lastConnectionDate": "2019-01-18T15:06:33+0100",
-          "local": false,
-          "login": "obi",
-          "managed": false,
-          "name": "One",
-          "scmAccounts": [],
-          "sonarLintLastConnectionDate": "2019-01-16T15:06:33+0100",
-        }
-      }
-    />
-  </td>
-</tr>
-`;
-
-exports[`should render correctly without last connection date 1`] = `
-<tr>
-  <td
-    className="thin text-middle"
-  >
-    <div
-      className="sw-flex sw-items-center"
-    >
-      <withAppStateContext(Avatar)
-        className="sw-shrink-0 sw-mr-4"
-        name="One"
-        size={36}
-      />
-      <UserListItemIdentity
-        user={
-          {
-            "active": true,
-            "lastConnectionDate": "2019-01-18T15:06:33+0100",
-            "local": false,
-            "login": "obi",
-            "managed": false,
-            "name": "One",
-            "scmAccounts": [],
-            "sonarLintLastConnectionDate": "2019-01-16T15:06:33+0100",
-          }
-        }
-      />
-    </div>
-  </td>
-  <td
-    className="thin text-middle"
-  >
-    <UserScmAccounts
-      scmAccounts={[]}
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <DateFromNow
-      date="2019-01-18T15:06:33+0100"
-      hourPrecision={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <DateFromNow
-      date="2019-01-16T15:06:33+0100"
-      hourPrecision={true}
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <UserGroups
-      groups={[]}
-      onUpdateUsers={[MockFunction]}
-      user={
-        {
-          "active": true,
-          "lastConnectionDate": "2019-01-18T15:06:33+0100",
-          "local": false,
-          "login": "obi",
-          "managed": false,
-          "name": "One",
-          "scmAccounts": [],
-          "sonarLintLastConnectionDate": "2019-01-16T15:06:33+0100",
-        }
-      }
-    />
-  </td>
-  <td
-    className="thin nowrap text-middle"
-  >
-    <ButtonIcon
-      aria-label="users.update_tokens_for_x.One"
-      className="js-user-tokens spacer-left button-small"
-      onClick={[Function]}
-      tooltip="users.update_tokens"
-    >
-      <BulletListIcon />
-    </ButtonIcon>
-  </td>
-  <td
-    className="thin nowrap text-right text-middle"
-  >
-    <UserActions
-      isCurrentUser={false}
-      onUpdateUsers={[MockFunction]}
-      user={
-        {
-          "active": true,
-          "lastConnectionDate": "2019-01-18T15:06:33+0100",
-          "local": false,
-          "login": "obi",
-          "managed": false,
-          "name": "One",
-          "scmAccounts": [],
-          "sonarLintLastConnectionDate": "2019-01-16T15:06:33+0100",
-        }
-      }
-    />
-  </td>
-</tr>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap
deleted file mode 100644 (file)
index 48791a5..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`#ExternalProvider should render correctly 1`] = `
-<div
-  className="js-user-identity-provider little-spacer-top"
->
-  <div
-    className="identity-provider"
-    style={
-      {
-        "backgroundColor": "blue",
-        "color": "#fff",
-      }
-    }
-  >
-    <img
-      alt="Foo Provider"
-      className="little-spacer-right"
-      height="14"
-      src="icon/path"
-      width="14"
-    />
-  </div>
-</div>
-`;
-
-exports[`#ExternalProvider should render the user external provider and identity 1`] = `
-<div
-  className="js-user-identity-provider little-spacer-top"
->
-  <span>
-    foo
-    : 
-  </span>
-</div>
-`;
-
-exports[`#UserListItemIdentity should render correctly 1`] = `
-<div>
-  <div>
-    <strong
-      className="js-user-name"
-    >
-      One
-    </strong>
-    <span
-      className="js-user-login note little-spacer-left"
-    >
-      obi
-    </span>
-  </div>
-  <div
-    className="js-user-email little-spacer-top"
-  >
-    obi.one@empire
-  </div>
-  <ExternalProvider
-    identityProvider={
-      {
-        "backgroundColor": "blue",
-        "iconPath": "icon/path",
-        "key": "foo",
-        "name": "Foo Provider",
-      }
-    }
-    user={
-      {
-        "active": true,
-        "email": "obi.one@empire",
-        "externalProvider": "foo",
-        "lastConnectionDate": "2019-01-18T15:06:33+0100",
-        "local": false,
-        "login": "obi",
-        "managed": false,
-        "name": "One",
-        "scmAccounts": [],
-      }
-    }
-  />
-</div>
-`;
index fe4dd38fa583fb4a656290bdc072cd0dc69ffccb..b3948def2ca0c8814372eab36f9ba538f25c3945 100644 (file)
@@ -53,6 +53,7 @@ import {
   SysInfoCluster,
   SysInfoLogging,
   SysInfoStandalone,
+  UserGroupMember,
 } from '../types/types';
 import { CurrentUser, LoggedInUser, User } from '../types/users';
 
@@ -676,6 +677,16 @@ export function mockUser(overrides: Partial<User> = {}): User {
   };
 }
 
+export function mockUserGroupMember(overrides: Partial<UserGroupMember> = {}): UserGroupMember {
+  return {
+    login: 'john.doe',
+    name: 'John Doe',
+    managed: false,
+    selected: true,
+    ...overrides,
+  };
+}
+
 export function mockDocumentationMarkdown(
   overrides: Partial<{ content: string; title: string; key: string }> = {}
 ): string {